├── .github
├── dependabot.yaml
└── workflows
│ ├── docker.yml
│ ├── pre-commit-autoupdate.yml
│ ├── pre-commit.yml
│ ├── pypi_release.yml
│ ├── python_safety.yml
│ ├── shellcheck.yml
│ ├── test_python.yml
│ ├── ubuntu_build.yml
│ └── update_locales.yml
├── .gitignore
├── .pre-commit-config.yaml
├── AUTHORS
├── LICENSE
├── Makefile
├── README.md
├── docker
├── Dockerfile
├── README.md
├── docker-compose.override.yml
├── docker-compose.yml
├── entrypoint.sh
└── motioneye-docker.conf
├── l10n
├── babel.cfg
├── po2json
├── traduki_js.txt
├── traduki_po.sh
├── traduki_python.txt
├── traduko.sh
└── v4l2.js
├── logo
├── logo-color.svg
└── logo-simple.svg
├── motioneye
├── .gitignore
├── __init__.py
├── cleanup.py
├── config.py
├── controls
│ ├── __init__.py
│ ├── diskctl.py
│ ├── mmalctl.py
│ ├── powerctl.py
│ ├── smbctl.py
│ ├── tzctl.py
│ ├── v4l2ctl.py
│ └── wifictl.py
├── extra
│ ├── linux_init
│ ├── motioneye.conf.sample
│ ├── motioneye.systemd
│ └── motioneye.sysv
├── handlers
│ ├── __init__.py
│ ├── action.py
│ ├── base.py
│ ├── config.py
│ ├── log.py
│ ├── login.py
│ ├── main.py
│ ├── movie.py
│ ├── movie_playback.py
│ ├── picture.py
│ ├── power.py
│ ├── prefs.py
│ ├── relay_event.py
│ ├── update.py
│ └── version.py
├── locale
│ ├── ar
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── bn
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── ca
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── cs
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── de
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── el
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── en
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── es
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── fi
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── fr
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── hi
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── hu
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── it
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── ja
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── ko
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── motioneye.js.pot
│ ├── motioneye.pot
│ ├── ms
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── nb_NO
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── nl
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── pa
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── pl
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── pt
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── ro
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── ru
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── sk
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── sv
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── ta
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── tr
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── uk
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ ├── vi
│ │ └── LC_MESSAGES
│ │ │ ├── motioneye.js.po
│ │ │ ├── motioneye.mo
│ │ │ └── motioneye.po
│ └── zh
│ │ └── LC_MESSAGES
│ │ ├── motioneye.js.po
│ │ ├── motioneye.mo
│ │ └── motioneye.po
├── mediafiles.py
├── meyectl.py
├── mjpgclient.py
├── monitor.py
├── motionctl.py
├── motioneye_init.py
├── prefs.py
├── remote.py
├── scripts
│ ├── migrateconf.sh
│ └── relayevent.sh
├── sendmail.py
├── sendtelegram.py
├── server.py
├── settings.py
├── shell.py
├── static
│ ├── css
│ │ ├── frame.css
│ │ ├── jquery.timepicker.min.css
│ │ ├── main.css
│ │ └── ui.css
│ ├── fnt
│ │ ├── mavenpro-black-webfont.woff
│ │ ├── mavenpro-bold-webfont.woff
│ │ ├── mavenpro-medium-webfont.woff
│ │ └── mavenpro-regular-webfont.woff
│ ├── img
│ │ ├── IEC5007_On_Symbol.svg
│ │ ├── IEC5008_Off_Symbol.svg
│ │ ├── apply-progress.gif
│ │ ├── arrows.svg
│ │ ├── camera-action-button-alarm-off.svg
│ │ ├── camera-action-button-alarm-on.svg
│ │ ├── camera-action-button-down.svg
│ │ ├── camera-action-button-left.svg
│ │ ├── camera-action-button-light-off.svg
│ │ ├── camera-action-button-light-on.svg
│ │ ├── camera-action-button-lock.svg
│ │ ├── camera-action-button-preset.svg
│ │ ├── camera-action-button-record-start.svg
│ │ ├── camera-action-button-record-stop.svg
│ │ ├── camera-action-button-right.svg
│ │ ├── camera-action-button-snapshot.svg
│ │ ├── camera-action-button-unlock.svg
│ │ ├── camera-action-button-up.svg
│ │ ├── camera-action-button-zoom-in.svg
│ │ ├── camera-action-button-zoom-out.svg
│ │ ├── camera-progress.gif
│ │ ├── camera-top-buttons.svg
│ │ ├── combo-box-arrow.svg
│ │ ├── main-loading-progress.gif
│ │ ├── modal-progress.gif
│ │ ├── motioneye-icon.svg
│ │ ├── motioneye-logo.svg
│ │ ├── no-camera.svg
│ │ ├── no-preview.svg
│ │ ├── slider-arrow.svg
│ │ ├── small-progress.gif
│ │ ├── top-bar-buttons.svg
│ │ └── validation-error.svg
│ └── js
│ │ ├── css-browser-selector.min.js
│ │ ├── frame.js
│ │ ├── gettext.min.js
│ │ ├── jquery.min.js
│ │ ├── jquery.mousewheel.min.js
│ │ ├── jquery.timepicker.min.js
│ │ ├── main.js
│ │ ├── motioneye.ar.json
│ │ ├── motioneye.bn.json
│ │ ├── motioneye.ca.json
│ │ ├── motioneye.cs.json
│ │ ├── motioneye.de.json
│ │ ├── motioneye.el.json
│ │ ├── motioneye.en.json
│ │ ├── motioneye.es.json
│ │ ├── motioneye.fi.json
│ │ ├── motioneye.fr.json
│ │ ├── motioneye.hi.json
│ │ ├── motioneye.hu.json
│ │ ├── motioneye.it.json
│ │ ├── motioneye.ja.json
│ │ ├── motioneye.ko.json
│ │ ├── motioneye.ms.json
│ │ ├── motioneye.nb_NO.json
│ │ ├── motioneye.nl.json
│ │ ├── motioneye.pa.json
│ │ ├── motioneye.pl.json
│ │ ├── motioneye.pt.json
│ │ ├── motioneye.ro.json
│ │ ├── motioneye.ru.json
│ │ ├── motioneye.sk.json
│ │ ├── motioneye.sv.json
│ │ ├── motioneye.ta.json
│ │ ├── motioneye.tr.json
│ │ ├── motioneye.uk.json
│ │ ├── motioneye.vi.json
│ │ ├── motioneye.zh.json
│ │ ├── ui.js
│ │ └── version.js
├── tasks.py
├── template.py
├── templates
│ ├── base.html
│ ├── main.html
│ ├── manifest.json
│ └── version.html
├── update.py
├── uploadservices.py
├── utils
│ ├── __init__.py
│ ├── dtconv.py
│ ├── http.py
│ ├── mjpeg.py
│ ├── rtmp.py
│ └── rtsp.py
├── webhook.py
└── wsswitch.py
├── pyproject.toml
├── setup.cfg
└── tests
├── __init__.py
├── test_handlers
├── __init__.py
├── test_base.py
└── test_login.py
└── test_utils
├── __init__.py
├── test_http.py
├── test_mjpeg.py
└── test_rtmp.py
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 |
8 | updates:
9 | - package-ecosystem: "github-actions"
10 | # Workflow files stored in the default location of `.github/workflows`
11 | directory: "/"
12 | schedule:
13 | interval: "daily"
14 | target-branch: "dev"
15 | labels:
16 | - "CI/CD"
17 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker
2 |
3 | on: [pull_request, push]
4 |
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}
7 | cancel-in-progress: true
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | docker:
14 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login
15 | runs-on: ubuntu-24.04
16 |
17 | permissions:
18 | packages: write
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: Docker meta
24 | id: meta
25 | uses: docker/metadata-action@v5
26 | with:
27 | images: | # add potential Docker Hub image
28 | ghcr.io/${{ github.repository_owner }}/motioneye
29 | tags: |
30 | type=edge,branch=dev
31 | type=semver,pattern={{version}}
32 | type=semver,pattern={{major}}.{{minor}}
33 | type=semver,pattern={{major}}
34 |
35 | - uses: docker/setup-qemu-action@v3
36 | - uses: docker/setup-buildx-action@v3
37 |
38 | - name: Cache Docker layers
39 | uses: actions/cache@v4
40 | with:
41 | path: |
42 | ${{ runner.temp }}/.buildx-cache
43 | key: ${{ runner.os }}-buildx-${{ github.sha }}
44 | restore-keys: |
45 | ${{ runner.os }}-buildx-
46 |
47 | #- name: Login to Docker Hub
48 | # uses: docker/login-action@v3
49 | # if: github.event_name == 'push' && github.repository == 'motioneye-project/motioneye' && steps.meta.outputs.tags != null
50 | # with:
51 | # username: ${{ secrets.DOCKER_USERNAME }}
52 | # password: ${{ secrets.DOCKER_TOKEN }}
53 |
54 | - name: Login to GitHub Container Registry
55 | uses: docker/login-action@v3
56 | if: github.event_name == 'push' && github.repository == 'motioneye-project/motioneye' && steps.meta.outputs.tags != null
57 | with:
58 | registry: ghcr.io
59 | username: ${{ github.actor }}
60 | password: ${{ github.token }}
61 |
62 | - name: Build
63 | uses: docker/build-push-action@v6
64 | with:
65 | context: .
66 | file: ./docker/Dockerfile
67 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/riscv64
68 | push: ${{ github.event_name == 'push' && github.repository == 'motioneye-project/motioneye' && steps.meta.outputs.tags != null }}
69 | tags: ${{ steps.meta.outputs.tags }}
70 | labels: ${{ steps.meta.outputs.labels }}
71 | #build-args: |
72 | # KEY1=Value1
73 | # KEY2=Value2
74 | cache-from: type=local,src=${{ runner.temp }}/.buildx-cache
75 | cache-to: type=local,dest=${{ runner.temp }}/.buildx-cache-new
76 |
77 | - name: Move cache
78 | run: |
79 | rm -rf ${{ runner.temp }}/.buildx-cache
80 | mv ${{ runner.temp }}/.buildx-cache-new ${{ runner.temp }}/.buildx-cache
81 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit-autoupdate.yml:
--------------------------------------------------------------------------------
1 | name: pre-commit-autoupdate
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '42 15 * * *'
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | permissions:
13 | pull-requests: write
14 |
15 | jobs:
16 | pre-commit-autoupdate:
17 | runs-on: ubuntu-24.04
18 | steps:
19 | - uses: actions/checkout@v4
20 | with:
21 | ref: dev
22 | # https://github.com/peter-evans/create-pull-request/issues/48
23 | token: ${{ secrets.GH_PAT }}
24 | - uses: actions/setup-python@v5
25 | with:
26 | python-version: '3.x'
27 | check-latest: true
28 | - env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 | run: |
31 | branch_exists=0
32 | git fetch origin pre-commit-autoupdate && branch_exists=1
33 | if (( branch_exists ))
34 | then
35 | git switch pre-commit-autoupdate
36 | else
37 | git checkout -b pre-commit-autoupdate
38 | fi
39 | pip install pre-commit
40 | pre-commit --version
41 | pre-commit autoupdate
42 | git diff --exit-code && exit 0
43 | git add -A
44 | git config user.name 'github-actions[bot]'
45 | git config user.email 'github-actions[bot]@users.noreply.github.com'
46 | if (( branch_exists ))
47 | then
48 | git commit --amend --no-edit
49 | git push -f origin pre-commit-autoupdate
50 | else
51 | git commit -m '[CI/CD] pre-commit autoupdate'
52 | git push origin pre-commit-autoupdate
53 | gh pr create -B dev -H pre-commit-autoupdate -f -l 'CI/CD'
54 | fi
55 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit.yml:
--------------------------------------------------------------------------------
1 | # https://pre-commit.com
2 | # This GitHub Action assumes that the repo contains a valid .pre-commit-config.yaml file.
3 | name: pre-commit
4 |
5 | on: [pull_request, push]
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: true
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | pre-commit:
16 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login
17 | runs-on: ubuntu-24.04
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: actions/setup-python@v5
21 | with:
22 | python-version: '3.x'
23 | check-latest: true
24 | - run: |
25 | pip install pre-commit
26 | pre-commit --version
27 | pre-commit run --all-files --show-diff-on-failure
28 |
--------------------------------------------------------------------------------
/.github/workflows/pypi_release.yml:
--------------------------------------------------------------------------------
1 | name: PyPI release
2 |
3 | on: workflow_dispatch
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-24.04
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-python@v5
14 | with:
15 | python-version: '3.x'
16 | check-latest: true
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip setuptools wheel
20 | python -m pip install --upgrade build twine
21 | - name: Build package
22 | run: python -m build
23 | - name: Publish package
24 | #run: twine upload -r testpypi -u '__token__' -p '${{ secrets.TEST_PYPI_API_TOKEN }}' dist/*
25 | run: twine upload -r pypi -u '__token__' -p '${{ secrets.PYPI_TOKEN }}' dist/*
26 |
--------------------------------------------------------------------------------
/.github/workflows/python_safety.yml:
--------------------------------------------------------------------------------
1 | name: python_safety
2 |
3 | on: [pull_request, push]
4 |
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}
7 | cancel-in-progress: true
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | python_safety:
14 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login
15 | runs-on: ubuntu-24.04
16 | steps:
17 | - run: echo -e '[global]\nbreak-system-packages=true' | sudo tee /etc/pip.conf # error: externally-managed-environment
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-python@v5
20 | with:
21 | python-version: 3.x
22 | check-latest: true
23 | - run: pip install --upgrade pip setuptools
24 | - run: pip install safety .
25 | # Ignore CVE-2018-20225, which is IMO reasonably disputed: https://data.safetycli.com/v/67599/97c/
26 | # "extra"-index-url means an index to "additionally" look for newer versions, pre-compiled wheels, or similar, not to force this index being used.
27 | # There is "index-url" to enforce a different index: https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-i
28 | # Ignore CVE-2019-8341 as well: https://github.com/pyupio/safety/issues/527
29 | - run: safety check --ignore 67599,70612
30 |
--------------------------------------------------------------------------------
/.github/workflows/shellcheck.yml:
--------------------------------------------------------------------------------
1 | name: shellcheck
2 |
3 | on: [pull_request, push]
4 |
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}
7 | cancel-in-progress: true
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | shellcheck:
14 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login
15 | runs-on: ubuntu-24.04
16 | steps:
17 | - name: Install xz-utils
18 | run: |
19 | sudo apt-get -q update
20 | sudo DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends install xz-utils
21 | - uses: actions/checkout@v4
22 | - name: Download shellcheck
23 | run: |
24 | curl -sSfL "$(curl -sSf 'https://api.github.com/repos/koalaman/shellcheck/releases/latest' | mawk -F\" '/"browser_download_url.*\.linux\.x86_64\.tar\.xz"/{print $4;exit}')" -o shellcheck.tar.xz
25 | tar --wildcards --strip-components=1 -xf shellcheck.tar.xz '*/shellcheck'
26 | rm shellcheck.tar.xz
27 | - name: Run shellcheck
28 | run: |
29 | mapfile -t FILES < <(find . -not \( -path './.git' -prune \) -type f) # read all files to array
30 | for i in "${!FILES[@]}"
31 | do
32 | [[ ${FILES[$i]##*/} =~ '.'[^.]*'sh'$ ]] && continue # file has shell extension
33 | [[ $(mawk 'NR==1 && $0 ~ /^#!.*sh([[:blank:]]|$)/{print;exit}' "${FILES[$i]}") ]] && continue # file has shell shebang
34 | unset -v "FILES[$i]" # else remove from array
35 | done
36 | ./shellcheck -xC -o all -e SC2244,SC2250,SC2312 "${FILES[@]}"
37 |
--------------------------------------------------------------------------------
/.github/workflows/test_python.yml:
--------------------------------------------------------------------------------
1 | name: test_python
2 |
3 | on: [pull_request, push]
4 |
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}
7 | cancel-in-progress: true
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | test:
14 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login
15 | strategy:
16 | matrix:
17 | include:
18 | # https://packages.ubuntu.com/search?suite=all&arch=any&searchon=names&keywords=python3
19 | - { dist: 'ubuntu-22.04', python: '3.10.6' }
20 | - { dist: 'ubuntu-24.04', python: '3.12.3' }
21 | - { dist: 'ubuntu-22.04-arm', python: '3.10.6' }
22 | - { dist: 'ubuntu-24.04-arm', python: '3.12.3' }
23 | fail-fast: false
24 | runs-on: ${{ matrix.dist }}
25 | name: "Test on ${{ matrix.dist }}"
26 | steps:
27 | - uses: actions/checkout@v4
28 | - uses: actions/setup-python@v5
29 | with:
30 | python-version: ${{ matrix.python }}
31 | - if: matrix.dist == 'ubuntu-22.04' || matrix.dist == 'ubuntu-24.04'
32 | run: sudo apt-mark hold grub-efi-amd64-signed # GRUB does not always find the drive it was configured for
33 | - name: Ubuntu Noble workarounds
34 | if: matrix.dist == 'ubuntu-24.04' || matrix.dist == 'ubuntu-24.04-arm'
35 | run: |
36 | # https://github.com/actions/runner-images/pull/9956
37 | sudo apt-get autopurge needrestart
38 | # error: externally-managed-environment
39 | echo -e '[global]\nbreak-system-packages=true' | sudo tee /etc/pip.conf
40 | - run: sudo apt-get -q update
41 | - run: sudo DEBIAN_FRONTEND="noninteractive" apt-get -qq --no-install-recommends dist-upgrade
42 | - run: sudo DEBIAN_FRONTEND="noninteractive" apt-get -qq --no-install-recommends install
43 | curl gcc ffmpeg libcurl4-openssl-dev libssl-dev motion v4l-utils
44 | - run: python3 -m pip install --upgrade pip setuptools wheel
45 | - run: python3 -m pip install --upgrade build mypy pytest
46 | - run: python3 -m build
47 | - run: python3 -m pip install .
48 | - run: mkdir --parents --verbose .mypy_cache
49 | - run: mypy --ignore-missing-imports --install-types --non-interactive --exclude build/ . || true
50 | - run: pytest --ignore=tests/test_utils/test_mjpeg.py
51 | --ignore=tests/test_utils/test_rtmp.py . || true
52 | - run: pytest --fixtures tests/test_utils/test_mjpeg.py || true
53 | - run: pytest --fixtures tests/test_utils/test_rtmp.py || true
54 | - run: pytest . || pytest --doctest-modules . || true
55 |
--------------------------------------------------------------------------------
/.github/workflows/ubuntu_build.yml:
--------------------------------------------------------------------------------
1 | name: ubuntu_build
2 |
3 | on: [pull_request, push]
4 |
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}
7 | cancel-in-progress: true
8 |
9 | permissions: {}
10 |
11 | jobs:
12 | build:
13 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login != github.event.pull_request.base.repo.owner.login
14 | strategy:
15 | matrix:
16 | dist: ['ubuntu-22.04', 'ubuntu-24.04', 'ubuntu-22.04-arm', 'ubuntu-24.04-arm']
17 | fail-fast: false
18 | runs-on: ${{ matrix.dist }}
19 | name: "Test on ${{ matrix.dist }}"
20 | steps:
21 | - if: matrix.dist == 'ubuntu-22.04' || matrix.dist == 'ubuntu-24.04'
22 | run: sudo apt-mark hold grub-efi-amd64-signed # GRUB does not always find the drive it was configured for
23 | - name: Ubuntu Noble workarounds
24 | if: matrix.dist == 'ubuntu-24.04' || matrix.dist == 'ubuntu-24.04-arm'
25 | run: |
26 | # https://github.com/actions/runner-images/pull/9956
27 | # ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
28 | # new firefox package pre-installation script subprocess returned error exit status 1
29 | sudo apt-get autopurge needrestart python3-pip python3-setuptools python3-wheel firefox
30 | # error: externally-managed-environment
31 | echo -e '[global]\nbreak-system-packages=true' | sudo tee /etc/pip.conf
32 | - run: sudo apt-get -q update
33 | - run: sudo DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends dist-upgrade
34 | - run: sudo DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends install
35 | ca-certificates curl python3-dev
36 | - run: curl -sSfO 'https://bootstrap.pypa.io/get-pip.py'
37 | - run: sudo python3 get-pip.py
38 | - run: sudo python3 -m pip install --upgrade pip setuptools wheel
39 | - run: |
40 | REPO=$GITHUB_REPOSITORY BRANCH=$GITHUB_REF_NAME
41 | [ ${{ github.event_name }} = 'pull_request' ] && REPO=${{ github.event.pull_request.head.repo.full_name }} BRANCH=${{ github.event.pull_request.head.ref }}
42 | sudo python3 -m pip install "https://github.com/$REPO/archive/$BRANCH.tar.gz"
43 | - run: sudo motioneye_init --skip-apt-update
44 | - run: i=0; until ss -tln | grep 8765; do [ $i -le 10 ] || exit 0; sleep 1; i=$(expr $i + 1); done
45 | - run: sudo systemctl status motioneye
46 | - run: sudo systemctl is-active motioneye
47 |
--------------------------------------------------------------------------------
/.github/workflows/update_locales.yml:
--------------------------------------------------------------------------------
1 | name: update_locales
2 |
3 | on: pull_request
4 |
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.ref }}
7 | cancel-in-progress: true
8 |
9 | permissions:
10 | contents: read
11 |
12 | defaults:
13 | run:
14 | shell: sh
15 |
16 | jobs:
17 | update_locales:
18 | # Skip for forks and dependabot which have no access to secrets (PAT)
19 | if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'
20 | runs-on: ubuntu-24.04
21 | steps:
22 |
23 | - uses: actions/setup-python@v5
24 | with:
25 | python-version: '3.x'
26 | check-latest: true
27 |
28 | - name: Install dependencies
29 | run: |
30 | python3 -m pip install --upgrade pip setuptools
31 | python3 -m pip install --upgrade babel jinja2
32 | sudo apt-get -q update
33 | sudo DEBIAN_FRONTEND='noninteractive' apt-get -qq --no-install-recommends install gettext
34 |
35 | - uses: actions/checkout@v4
36 | with:
37 | ref: ${{ github.head_ref }}
38 | # https://github.com/peter-evans/create-pull-request/issues/48
39 | token: ${{ secrets.GH_PAT }}
40 |
41 | - name: Generate backend template
42 | run: |
43 | pybabel extract -F l10n/babel.cfg -o motioneye/locale/motioneye.pot motioneye/
44 | # Remove trailing empty line to satisfy pre-commit
45 | sed -i '${/^$/d}' motioneye/locale/motioneye.pot
46 |
47 | - name: Generate frontend template
48 | run: xgettext --no-wrap --from-code=UTF-8 -o motioneye/locale/motioneye.js.pot motioneye/static/js/*.js l10n/*.js
49 |
50 | - name: Generate frontend locales
51 | run: |
52 | for i in motioneye/locale/*/LC_MESSAGES/motioneye.js.po
53 | do
54 | lang=${i#motioneye/locale/}
55 | lang=${lang%/LC_MESSAGES/motioneye.js.po}
56 | echo "Generating motioneye.$lang.json"
57 | l10n/po2json "$i" "motioneye/static/js/motioneye.$lang.json"
58 | done
59 |
60 | - name: Commit changes
61 | run: |
62 | git add -NA
63 | git diff -I '^"POT-Creation-Date: ' --exit-code && exit 0
64 | git config user.name 'github-actions[bot]'
65 | git config user.email 'github-actions[bot]@users.noreply.github.com'
66 | git add -A
67 | git commit -m 'Update translation files'
68 | git push
69 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.py[cod]
3 | __pycache__
4 | *.bak
5 | *.pid
6 | *.pot
7 | *.po~
8 | *.old
9 | toto*
10 | .project
11 | .pydevproject
12 | .settings
13 | .idea
14 | run
15 | build
16 | dist
17 | dropbox.keys
18 | .venv
19 | _traduko.jar
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # Learn more about this config here: https://pre-commit.com/
2 |
3 | # To enable these pre-commit hook run:
4 | # `brew install pre-commit` or `python3 -m pip install pre-commit`
5 | # Then in the project root directory run `pre-commit install`
6 |
7 | # default_language_version:
8 | # python: python3.10
9 |
10 | repos:
11 | - repo: https://github.com/pre-commit/pre-commit-hooks
12 | rev: v5.0.0
13 | hooks:
14 | - id: check-builtin-literals
15 | - id: check-executables-have-shebangs
16 | - id: check-shebang-scripts-are-executable
17 | - id: check-yaml
18 | - id: detect-private-key
19 | - id: end-of-file-fixer
20 | - id: mixed-line-ending
21 | - id: trailing-whitespace
22 | - repo: https://github.com/PyCQA/bandit
23 | rev: 1.8.3
24 | hooks:
25 | - id: bandit
26 | args:
27 | - --skip=B104,B105,B108,B110,B301,B310,B321,B324,B402,B403,B404,B602,B603,B604,B605,B607,B701
28 | - repo: https://github.com/python/black
29 | rev: 25.1.0
30 | hooks:
31 | - id: black
32 | args: [--skip-string-normalization]
33 | - repo: https://github.com/codespell-project/codespell
34 | rev: v2.4.1
35 | hooks:
36 | - id: codespell
37 | # See args in setup.cfg
38 | - repo: https://github.com/PyCQA/flake8
39 | rev: 7.2.0
40 | hooks:
41 | - id: flake8
42 | additional_dependencies: [flake8-2020, flake8-bugbear, flake8-comprehensions, flake8-return]
43 | args:
44 | - --builtins=_
45 | - --count
46 | - --max-complexity=68
47 | - --max-line-length=125
48 | - --select=C901,E501,E9,F63,F7,F82
49 | - --show-source
50 | - --statistics
51 | - repo: https://github.com/timothycrosley/isort
52 | rev: 6.0.1
53 | hooks:
54 | - id: isort
55 | args: ["--profile", "black", "--filter-files"]
56 | - repo: https://github.com/asottile/pyupgrade
57 | rev: v3.19.1
58 | hooks:
59 | - id: pyupgrade
60 | args: [--py37-plus]
61 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | A list with contributors can be found here: https://github.com/motioneye-project/motioneye/graphs/contributors
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | all: motioneye/locale/*/LC_MESSAGES/motioneye.mo motioneye/static/js/motioneye.*.json
3 |
4 | %.mo: %.po
5 | msgfmt -f $*.po -o $*.mo
6 |
7 | %/motioneye.po: motioneye/locale/motioneye.pot
8 | msgmerge --no-wrap -N -U $@ $<
9 | # Disable Google Translator usage for now, which does rarely work from GitHub CI due to rate limiting.
10 | # Also Weblate supports auto-translation from various sources as well.
11 | #l10n/traduki_po.sh $@
12 |
13 | motioneye/static/js/motioneye.%.json: motioneye/locale/%/LC_MESSAGES/motioneye.js.po
14 | l10n/po2json motioneye/locale/$*/LC_MESSAGES/motioneye.js.po motioneye/static/js/motioneye.$*.json
15 |
16 | %/motioneye.js.po: motioneye/locale/motioneye.js.pot
17 | msgmerge --no-wrap -N -U $@ $<
18 | # Disable Google Translator usage for now, which does rarely work from GitHub CI due to rate limiting.
19 | # Also Weblate supports auto-translation from various sources as well.
20 | #l10n/traduki_po.sh $@
21 |
22 | motioneye/locale/motioneye.js.pot: motioneye/static/js/*.js l10n/*.js
23 | xgettext --no-wrap --from-code=UTF-8 -o motioneye/locale/motioneye.js.pot motioneye/static/js/*.js l10n/*.js
24 |
25 | motioneye/locale/motioneye.pot: motioneye/*.py motioneye/*/*.py motioneye/templates/*.html
26 | pybabel extract -F l10n/babel.cfg -o motioneye/locale/motioneye.pot motioneye/
27 | # Remove trailing empty line to satisfy pre-commit
28 | sed -i '$${/^$$/d}' motioneye/locale/motioneye.pot
29 |
30 | #####
31 | # regulo por krei novan tradukon
32 | # ekz. : uzi "make initro" por krei la rumana traduko.
33 | #####
34 | init%:
35 | mkdir motioneye/locale/$*
36 | mkdir motioneye/locale/$*/LC_MESSAGES
37 | msginit --no-wrap -i motioneye/locale/motioneye.js.pot -o motioneye/locale/$*.js.tmp -l$* --no-translator
38 | #l10n/traduki_po.sh motioneye/locale/$*.js.tmp
39 | mv motioneye/locale/$*.js.tmp motioneye/locale/$*/LC_MESSAGES/motioneye.js.po
40 | make motioneye/static/js/motioneye.$*.json
41 | msginit --no-wrap -i motioneye/locale/motioneye.pot -o motioneye/locale/$*.tmp -l$* --no-translator
42 | #l10n/traduki_po.sh motioneye/locale/$*.tmp
43 | mv motioneye/locale/$*.tmp motioneye/locale/$*/LC_MESSAGES/motioneye.po
44 | make motioneye/locale/$*/LC_MESSAGES/motioneye.mo
45 | #msgattrib --no-wrap --set-fuzzy --clear-obsolete locale/$*.tmp -o locale/$*/LC_MESSAGES/motioneye.po
46 |
47 | traduki:
48 | find motioneye/locale -name "*.po" -exec l10n/traduki_po.sh {} \;
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # What is motionEye?
2 |
3 | **motionEye** is an online interface for the software [_motion_](https://motion-project.github.io/), a video surveillance program with motion detection.
4 |
5 | Check out the [__wiki__](https://github.com/motioneye-project/motioneye/wiki) for more details. Changelog is available on the [__releases page__](https://github.com/motioneye-project/motioneye/releases).
6 |
7 | From version 0.43, **motionEye** is multilingual:
8 |
9 | | [
](https://hosted.weblate.org/engage/motioneye-project/) |
10 | | -: |
11 |
12 | You can contribute to translations on [__Weblate__](https://hosted.weblate.org/projects/motioneye-project).
13 |
14 | # Installation
15 |
16 | 1. Install **Python 3.7 or later** and build dependencies
17 |
18 | _Here the commands for APT-based Linux distributions are given._
19 |
20 | Thanks to pre-compiled wheels from PyPI, installing motionEye usually does not require anything but Python 3 and cURL with the ability to do HTTPS network requests:
21 | ```sh
22 | sudo apt update
23 | sudo apt --no-install-recommends install ca-certificates curl python3
24 | ```
25 |
26 | On **ARMv6 and ARMv7 (32-bit), RISC-V and other rare CPU architectures** additional build dependencies may be required to compile the [Pillow](https://pypi.org/project/pillow/) and [PycURL](https://pypi.org/project/pycurl/) modules:
27 | ```sh
28 | sudo apt update
29 | sudo apt --no-install-recommends install ca-certificates curl python3 python3-dev gcc libjpeg62-turbo-dev libcurl4-openssl-dev libssl-dev
30 | ```
31 |
32 | 2. Install the Python package manager `pip`
33 | ```sh
34 | curl -sSfO 'https://bootstrap.pypa.io/get-pip.py'
35 | sudo python3 get-pip.py
36 | rm get-pip.py
37 | ```
38 |
39 | **On recent Debian (Bookworm ant later) and Ubuntu (Lunar and later) versions**, the `libpython3.*-stdlib` package ships a file `/usr/lib/python3.*/EXTERNALLY-MANAGED`, which prevents the installation of Python modules outside of `venv` environments.
40 | motionEye however has a small number of dependencies with no strict version requirements and hence is very unlikely to break any Python package you might have installed via APT. To bypass this block, add `break-system-packages=true` to the `[global]` section of your `pip.conf`:
41 | ```sh
42 | grep -q '\[global\]' /etc/pip.conf 2> /dev/null || printf '%b' '[global]\n' | sudo tee -a /etc/pip.conf > /dev/null
43 | sudo sed -i '/^\[global\]/a\break-system-packages=true' /etc/pip.conf
44 | ```
45 |
46 | 3. Install and setup **motionEye**
47 | ```sh
48 | sudo python3 -m pip install --pre motioneye
49 | sudo motioneye_init
50 | ```
51 | _NB: `motioneye_init` currently assumes either an APT- or RPM-based distribution with `systemd` as init system. For a manual setup, config and service files can be found here: _
52 |
53 | # Upgrade
54 |
55 | ```sh
56 | sudo systemctl stop motioneye
57 | sudo python3 -m pip install --upgrade --pre motioneye
58 | sudo systemctl start motioneye
59 | ```
60 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:trixie-slim
2 | LABEL maintainer="Marcus Klein "
3 |
4 | # By default, run as root
5 | ARG RUN_UID=0
6 | ARG RUN_GID=0
7 |
8 | COPY . /tmp/motioneye
9 | COPY docker/entrypoint.sh /entrypoint.sh
10 |
11 | # Build deps:
12 | # - armhf/riscv64: Python headers, C compiler and libjpeg for Pillow: https://pypi.org/project/pillow/#files, libcurl and libssl for pycurl: https://pypi.org/project/pycurl/#files
13 | RUN printf '%b' '[global]\nbreak-system-packages=true\n' > /etc/pip.conf && \
14 | case "$(dpkg --print-architecture)" in \
15 | 'armhf'|'riscv64') PACKAGES='python3-dev gcc libjpeg62-turbo-dev libcurl4-openssl-dev libssl-dev';; \
16 | *) PACKAGES='';; \
17 | esac && \
18 | apt-get -q update && \
19 | DEBIAN_FRONTEND="noninteractive" apt-get -qq --option Dpkg::Options::="--force-confnew" --no-install-recommends install \
20 | ca-certificates curl python3 fdisk $PACKAGES && \
21 | curl -sSfO 'https://bootstrap.pypa.io/get-pip.py' && \
22 | python3 get-pip.py && \
23 | python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel && \
24 | python3 -m pip install --no-cache-dir /tmp/motioneye && \
25 | motioneye_init --skip-systemd --skip-apt-update && \
26 | # Change uid/gid of user/group motion to match our desired IDs. This will
27 | # make it easier to use execute motion as our desired user later.
28 | sed -i "s/^\(motion:[^:]*\):[0-9]*:[0-9]*:\(.*\)/\1:${RUN_UID}:${RUN_GID}:\2/" /etc/passwd && \
29 | sed -i "s/^\(motion:[^:]*\):[0-9]*:\(.*\)/\1:${RUN_GID}:\2/" /etc/group && \
30 | mv /etc/motioneye/motioneye.conf /etc/motioneye.conf.sample && \
31 | mkdir /var/log/motioneye /var/lib/motioneye && \
32 | chown motion:motion /var/log/motioneye /var/lib/motioneye && \
33 | # Cleanup
34 | python3 -m pip uninstall -y pip setuptools wheel && \
35 | DEBIAN_FRONTEND="noninteractive" apt-get -qq autopurge $PACKAGES && \
36 | apt-get clean && \
37 | rm -r /var/lib/apt/lists /var/cache/apt /tmp/motioneye get-pip.py /root/.cache
38 |
39 | # R/W needed for motionEye to update configurations
40 | VOLUME /etc/motioneye
41 |
42 | # Video & images
43 | VOLUME /var/lib/motioneye
44 |
45 | EXPOSE 8765
46 |
47 | ENTRYPOINT ["/entrypoint.sh"]
48 |
--------------------------------------------------------------------------------
/docker/README.md:
--------------------------------------------------------------------------------
1 | # Docker instructions
2 |
3 | ## Running from the official images
4 |
5 | The easiest way to run motionEye under docker is to use the official images.
6 | The command below will run motionEye and preserve your configuration and videos
7 | across motionEye restarts.
8 |
9 | ```bash
10 | docker pull ccrisan/motioneye:master-amd64
11 | docker run \
12 | --rm \
13 | -d \
14 | -p 8765:8765 \
15 | --hostname="motioneye" \
16 | -v /etc/localtime:/etc/localtime:ro \
17 | -v /data/motioneye/config:/etc/motioneye \
18 | -v /data/motioneye/videos:/var/lib/motioneye \
19 | ccrisan/motioneye:master-amd64
20 | ```
21 |
22 | This configuration maps motionEye configs into the `/data/motioneye/config`
23 | directory on the host. Videos will be saved under `/data/motioneye/videos`.
24 | Change the directories to suit your particular needs and make sure those
25 | directories exist on the host before you start the container.
26 |
27 | Some may prefer to use docker volumes instead of mapped directories on the
28 | host. You can easily accomplish this by using the commands below:
29 |
30 | ```bash
31 | docker volume create motioneye-config
32 | docker volume create motioneye-videos
33 | docker pull ccrisan/motioneye:master-amd64
34 | docker run \
35 | --rm \
36 | -d \
37 | -p 8765:8765 \
38 | --hostname="motioneye" =
39 | -v /etc/localtime:/etc/localtime:ro \
40 | --mount type=volume,source=motioneye-config,destination=/etc/motioneye \
41 | --mount type=volume,source=motioneye-videos,destination=/var/lib/motioneye \
42 | ccrisan/motioneye:master-amd64
43 | ```
44 |
45 | Use `docker volume ls` to view existing volumes.
46 |
47 | ## Building your own image
48 |
49 | It's also possible to build your own motionEye docker image. This allows the
50 | use of UIDs other than root for the `motion` and `meyectl` daemons (the default
51 | on official images). If you want to use a non-privileged user/group for
52 | motionEye, *please make sure that user/group exist on the host* before running
53 | the commands below.
54 |
55 | For the examples below, we assume user `motion` and group `motion` exist on the host server.
56 |
57 | ```bash
58 | RUN_USER="motion"
59 | RUN_UID=$(id -u ${RUN_USER})
60 | RUN_GID=$(id -g ${RUN_USER})
61 | TIMESTAMP="$(date '+%Y%m%d-%H%M')"
62 |
63 | cd /tmp && \
64 | git clone https://github.com/motioneye-project/motioneye.git && \
65 | cd motioneye && \
66 | docker build \
67 | --network host \
68 | --build-arg="RUN_UID=${RUN_UID?}" \
69 | --build-arg="RUN_GID=${RUN_GID?}" \
70 | -t "${USER?}/motioneye:${TIMESTAMP}" \
71 | --no-cache \
72 | -f docker/Dockerfile .
73 | ```
74 |
75 | This will create a local image called `your_username/motioneye:YYYYMMDD-HHMM`.
76 | You can run this image using the examples under "Running official images", but
77 | omitting the `docker pull` command and replacing
78 | `ccrisan/motioneye:master-amd64` with the name of the local image you just built.
79 |
--------------------------------------------------------------------------------
/docker/docker-compose.override.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 | services:
3 | motioneye:
4 | restart: unless-stopped
5 | image: ghcr.io/motioneye-project/motioneye:edge # https://github.com/motioneye-project/motioneye/pkgs/container/motioneye
6 | devices:
7 | - "/dev/video0:/dev/video0"
8 | - "/dev/video1:/dev/video1"
9 | volumes:
10 | - /etc/localtime:/etc/localtime:ro
11 |
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: "3.5"
3 | services:
4 | motioneye:
5 | # ToDo: Change from unstable dev/edge to stable GitHub registry release and Docker registry release, once available
6 | image: ghcr.io/motioneye-project/motioneye:edge # https://github.com/motioneye-project/motioneye/pkgs/container/motioneye
7 | ports:
8 | - "8081:8081"
9 | - "8765:8765"
10 | volumes:
11 | - etc_motioneye:/etc/motioneye
12 | - var_lib_motioneye:/var/lib/motioneye
13 |
14 | volumes:
15 | etc_motioneye:
16 | var_lib_motioneye:
17 |
--------------------------------------------------------------------------------
/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | # We need to chown at startup time since volumes are mounted as root. This is fugly.
3 | mkdir -p /run/motioneye
4 | chown motion:motion /run/motioneye
5 | [ -f '/etc/motioneye/motioneye.conf' ] || cp -a /etc/motioneye.conf.sample /etc/motioneye/motioneye.conf
6 | exec su -g motion motion -s /bin/dash -c "LANGUAGE=en exec /usr/local/bin/meyectl startserver -c /etc/motioneye/motioneye.conf"
7 |
--------------------------------------------------------------------------------
/docker/motioneye-docker.conf:
--------------------------------------------------------------------------------
1 | description "motionEye Server in Docker container"
2 | author "Marcus Klein "
3 |
4 | start on filesystem and started docker
5 | stop on runlevel [!2345]
6 | respawn
7 | script
8 | /usr/bin/docker stop motioneye || true
9 | /usr/bin/docker rm motioneye || true
10 | docker run --name=motioneye \
11 | -p 8081:8081 \
12 | -p 8765:8765 \
13 | -h server \
14 | -e TZ="Europe/Berlin" \
15 | -v /docker/motioneye:/etc/motioneye \
16 | -v /var/lib/motioneye:/var/lib/motioneye \
17 | --restart=always \
18 | kleini/motioneye:docker
19 | end script
20 | pre-stop script
21 | if docker ps | grep -q motioneye
22 | then
23 | docker stop motioneye
24 | docker rm motioneye
25 | fi
26 | end script
27 |
--------------------------------------------------------------------------------
/l10n/babel.cfg:
--------------------------------------------------------------------------------
1 | [python: **.py]
2 | [jinja2: **/templates/**.html]
3 |
--------------------------------------------------------------------------------
/l10n/po2json:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | FICIN=$1
4 | FICOUT=$2
5 |
6 | awk '{
7 | if (CONTMSG==1 && substr($1,1,1) != "\"")
8 | {
9 | CONTMSG=0;
10 | }
11 | if (CONTMSG2==1 && substr($1,1,1) != "\"")
12 | {
13 | CONTMSG2=0;
14 | if (MSGID != "\"\"")
15 | print("," MSGID ":" MSGSTR);
16 | }
17 | if (substr($1,2,9) == "Language:")
18 | {
19 | print("{\"\":{\"language\":\"" substr($2,1,2) "\",\"plural-forms\":\"nplurals=2; plural=(n > 1);\"}");
20 | }
21 | else if ($1 == "msgid")
22 | {
23 | MSGID=substr($0,7);
24 | if (MSGID=="\"\"")
25 | CONTMSG=1;
26 | }
27 | else if (MSGID != "\"\"" && $1 == "msgstr")
28 | {
29 | MSGSTR=substr($0,7);
30 | if (MSGSTR=="\"\"")
31 | CONTMSG2=1;
32 | else
33 | print("," MSGID ":" MSGSTR);
34 | }
35 | else if (CONTMSG==1 && substr($1,1,1) == "\"")
36 | {
37 | MSGID=MSGID $0;
38 | }
39 | else if (CONTMSG2==1 && substr($1,1,1) == "\"")
40 | {
41 | MSGSTR=MSGSTR $0;
42 | }
43 | else
44 | {
45 | CONTMSG=0;
46 | CONTMSG2=0;
47 | }
48 | }
49 | END {
50 | print ("}");
51 | }' "$FICIN" > "$FICOUT"
52 |
--------------------------------------------------------------------------------
/l10n/traduki_js.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | Ni uzas por ĉiu lingvo json-dosieron, kiu enhavas la tradukojn.
4 | Ni ŝarĝas ĉi tiun dosieron en "templates/main.html":
5 |
6 |
7 |
19 |
20 | Kaj ni uzas la funkcion i18n.gettext donitan de gettext.min.js por traduki, ekz. :
21 | return i18n.gettext("Ĉi tiu kampo estas deviga");
22 |
23 | La dosiero "pot" estas ĝisdatigita per la komando xgettext, ekz. :
24 | xgettext --from-code=UTF-8 --no-wrap -o motioneye/locale/motioneye.js.pot static/js/*.js
25 | La dosiero "po" estas ĝisdatigita per la komando msgmerge, ekz. :
26 | msgmerge --no-wrap -N -U motioneye/locale/en/LC_MESSAGES/motioneye.js.po motioneye/locale/motioneye.js.pot
27 | Tradukoj povas esti ĝisdatigitaj per teksta redaktilo aŭ per poedit.
28 | La dosiero "json" estas ĝisdatigita per la komando scripts/po2json, ekz. :
29 | l10n/po2json motioneye/locale/en/LC_MESSAGES/motioneye.js.po motioneye/static/js/motioneye.en.json
30 |
31 | La dosiero "Makefile" permesas aŭtomate administri xgettext, msgmerge kaj po2json. Simple enigu "make" post modifi dosieron "js" aŭ "po".
32 |
33 | "Makefile" havas regulo "init%" por krei novan tradukon
34 | ekz. : uzi "make initro" por krei la rumana traduko.
35 |
--------------------------------------------------------------------------------
/l10n/traduki_po.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | #################################################################
3 | # skripto por aŭtomate traduki frazojn sen traduko en po-dosieron
4 | #################################################################
5 |
6 | src=eo
7 |
8 | #FIC=locale/en/LC_MESSAGES/motioneye.po
9 | FIC=$1
10 | dst=$(grep '^"Language: .*\n"$' "$FIC" | sed 's/^"Language: //;s/.n"$//')
11 |
12 | awk -v "src=$src" -v "dst=$dst" '{
13 | if (CONTMSG==1 && substr($1,1,1) != "\"")
14 | {
15 | CONTMSG=0;
16 | }
17 | if ($1 == "msgid")
18 | {
19 | MSGID=substr($0,7);
20 | if (MSGID=="\"\"")
21 | CONTMSG=1;
22 | }
23 | else if (CONTMSG==1 && substr($1,1,1) == "\"")
24 | {
25 | MSGID = substr(MSGID,1,length(MSGID)-1) substr($0,2);
26 | }
27 | else if ($1 == "msgstr")
28 | {
29 | if ($2 != "\"\"" || MSGID == "\"\"")
30 | {
31 | print ("msgid " MSGID);
32 | print $0;
33 | }
34 | else
35 | {
36 | getline nextline
37 | if (nextline == "")
38 | {
39 | print ("msgid " MSGID);
40 | printf("msgstr \"");
41 | MSG=system("l10n/traduko.sh " src " " dst " " MSGID)
42 | printf("\"\n\n");
43 | }
44 | else
45 | {
46 | print ("msgid " MSGID);
47 | print $0;
48 | print nextline;
49 | }
50 | }
51 | }
52 | else
53 | print $0;
54 | }' "$FIC" > "$FIC.$$"
55 | mv "$FIC" "$FIC.old"
56 | mv "$FIC.$$" "$FIC"
57 | # Remove trailing empty line, to satisfy pre-commit
58 | sed -i '${/^$/d}' "$FIC"
59 |
--------------------------------------------------------------------------------
/l10n/traduki_python.txt:
--------------------------------------------------------------------------------
1 |
2 | Objektivo: havi python-fonton en utf-8 kun multlingva gettext-administrado.
3 |
4 | la unua aŭ dua linio de .py devas enhavi: «coding: utf-8» aŭ «encoding: utf-8», ekz. :
5 | # This Python file uses the following encoding: utf-8
6 |
7 | kordoj enhavantaj specialajn signojn devas esti prefiksitaj per «u», ekz. :
8 | logging.fatal( _(u'bonvolu instali tornado version 3.1 aŭ pli') )
9 |
--------------------------------------------------------------------------------
/l10n/traduko.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | ################################################################
3 | # skripto por aŭtomate traduki frazon
4 | ################################################################
5 | #DEBUG=
6 |
7 | src=$1
8 | dst=$2
9 | txt=$3
10 |
11 | # Reuse cookie if not older than 15 minutes
12 |
13 | cookie=$(find . -maxdepth 1 -name _traduko.jar -mmin -14)
14 |
15 | # Obtain cookie
16 | [ "$cookie" ] || curl -sSfc _traduko.jar -A 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0' 'https://translate.google.com' -o /dev/null > /dev/null
17 |
18 | # Obtain translation from Google Translator API
19 | MSG0=$(curl -sSfb _traduko.jar -A 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0' \
20 | --refer 'https://translate.google.com/' \
21 | "https://translate.google.com/translate_a/single?client=webapp&sl=${src}&tl=${dst}&hl=${dst}&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&dt=gt&pc=1&otf=1&ssel=0&tsel=0&kc=1&tk=&ie=UTF-8&oe=UTF-8" \
22 | --data-urlencode "q=$txt" > /dev/null \
23 | )
24 |
25 | [ "$DEBUG" ] && printf '%s\n' "$src txt=$txt" >&2
26 |
27 | if printf '%s' "$MSG0" | grep -q 'sorry'
28 | then
29 | # Failed: Print error
30 | printf '%s\n%s\n' 'ERROR: Google Translator returned "sorry":' "$MSG0" >&2
31 | else
32 | # Success: Extract translated txt
33 | MSG=$(printf '%s' "$MSG0" | jq '.[0][][0]' | grep -v '^null$' \
34 | | sed "s/\\\\u003d/=/g;s/\\\\u003c//g" \
35 | | sed "s/\\\\u200b//g" \
36 | | sed "s/\xe2\x80\x8b//g" \
37 | | sed "s/^\"//;s/\"$//" \
38 | | tr -d "\n" \
39 | | sed "s/\\\ [nN]/n/g;s/] (/](/g;s/ __ / __/g" \
40 | | sed "s/\. \\\n$/. \\\n/" \
41 | )
42 | fi
43 |
44 | [ "$DEBUG" ] && printf '%s\n' "$dst txt=$MSG" >&2
45 |
46 | # Reset cookie if no message returned
47 | if [ "$MSG" ]
48 | then
49 | printf '%s' "$MSG"
50 | else
51 | printf '%s\n' 'ERROR: Google Translator did not return a translation' >&2
52 | rm -f _traduko.jar
53 | fi
54 |
--------------------------------------------------------------------------------
/l10n/v4l2.js:
--------------------------------------------------------------------------------
1 | /* fake file to allow translation of v4l2-ctl output */
2 | /* non exhaustive list of possible controls */
3 |
4 | i18n.gettext("Auto Exposure");
5 | i18n.gettext("Backlight Compensation");
6 | i18n.gettext("Brightness");
7 | i18n.gettext("Contrast");
8 | i18n.gettext("Exposure Absolute");
9 | i18n.gettext("Exposure Auto");
10 | i18n.gettext("Exposure Auto Priority");
11 | i18n.gettext("Exposure Time Absolute");
12 | i18n.gettext("Focus Absolute");
13 | i18n.gettext("Focus Auto");
14 | i18n.gettext("Gain");
15 | i18n.gettext("Gamma");
16 | i18n.gettext("Hue");
17 | i18n.gettext("Led1 Mode");
18 | i18n.gettext("Led1 Frequency");
19 | i18n.gettext("Pan Absolute");
20 | i18n.gettext("Power Line Frequency");
21 | i18n.gettext("Saturation");
22 | i18n.gettext("Sharpness");
23 | i18n.gettext("Tilt Absolute");
24 | i18n.gettext("White Balance Temperature");
25 | i18n.gettext("White Balance Temperature Auto");
26 | i18n.gettext("Zoom Absolute");
27 |
--------------------------------------------------------------------------------
/logo/logo-color.svg:
--------------------------------------------------------------------------------
1 |
46 |
--------------------------------------------------------------------------------
/logo/logo-simple.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/.gitignore:
--------------------------------------------------------------------------------
1 | traduko.jar
2 |
--------------------------------------------------------------------------------
/motioneye/__init__.py:
--------------------------------------------------------------------------------
1 | VERSION = "0.43.1b4"
2 |
--------------------------------------------------------------------------------
/motioneye/cleanup.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import datetime
18 | import logging
19 | import multiprocessing
20 | import os
21 | import signal
22 |
23 | from tornado.ioloop import IOLoop
24 |
25 | from motioneye import mediafiles, settings
26 |
27 | _process = None
28 |
29 |
30 | def start():
31 | if not settings.CLEANUP_INTERVAL:
32 | return
33 |
34 | # schedule the first call a bit later to improve performance at startup
35 | io_loop = IOLoop.current()
36 | io_loop.add_timeout(
37 | datetime.timedelta(seconds=min(settings.CLEANUP_INTERVAL, 60)), _run_process
38 | )
39 |
40 |
41 | def stop():
42 | global _process
43 |
44 | if not running():
45 | _process = None
46 | return
47 |
48 | if _process.is_alive():
49 | _process.join(timeout=10)
50 |
51 | if _process.is_alive():
52 | logging.error('cleanup process did not finish in time, killing it...')
53 | os.kill(_process.pid, signal.SIGKILL)
54 |
55 | _process = None
56 |
57 |
58 | def running():
59 | return _process is not None and _process.is_alive()
60 |
61 |
62 | def _run_process():
63 | global _process
64 |
65 | io_loop = IOLoop.current()
66 |
67 | # schedule the next call
68 | io_loop.add_timeout(
69 | datetime.timedelta(seconds=settings.CLEANUP_INTERVAL), _run_process
70 | )
71 |
72 | if not running(): # check that the previous process has finished
73 | logging.debug('running cleanup process...')
74 |
75 | _process = multiprocessing.Process(target=_do_cleanup)
76 | _process.start()
77 |
78 |
79 | def _do_cleanup():
80 | # this will be executed in a separate subprocess
81 |
82 | # ignore the terminate and interrupt signals in this subprocess
83 | signal.signal(signal.SIGINT, signal.SIG_IGN)
84 | signal.signal(signal.SIGTERM, signal.SIG_IGN)
85 |
86 | try:
87 | mediafiles.cleanup_media('picture')
88 | mediafiles.cleanup_media('movie')
89 | logging.debug('cleanup done')
90 |
91 | except Exception as e:
92 | logging.error(f'failed to cleanup media files: {str(e)}', exc_info=True)
93 |
--------------------------------------------------------------------------------
/motioneye/controls/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/controls/__init__.py
--------------------------------------------------------------------------------
/motioneye/controls/mmalctl.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from logging import debug
18 | from subprocess import CalledProcessError
19 |
20 | from motioneye import utils
21 |
22 |
23 | def list_devices():
24 | # currently MMAL support is designed specifically for the RPi;
25 | # therefore we can rely on the vcgencmd to report MMAL cameras
26 |
27 | debug('detecting MMAL camera')
28 |
29 | try:
30 | binary = utils.call_subprocess(['which', 'vcgencmd'])
31 |
32 | except CalledProcessError: # not found
33 | debug('unable to detect MMAL camera: vcgencmd has not been found')
34 | return []
35 |
36 | try:
37 | support = utils.call_subprocess([binary, 'get_camera'])
38 |
39 | except CalledProcessError: # not found
40 | debug('unable to detect MMAL camera: "vcgencmd get_camera" failed')
41 | return []
42 |
43 | if support.startswith('supported=1 detected=1'):
44 | debug('MMAL camera detected')
45 | return [('vc.ril.camera', 'VideoCore Camera')]
46 |
47 | return []
48 |
--------------------------------------------------------------------------------
/motioneye/controls/powerctl.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import logging
18 | import os
19 | import subprocess
20 | from collections import OrderedDict
21 | from typing import Dict
22 |
23 | from motioneye import utils
24 |
25 | __all__ = ('PowerControl',)
26 |
27 |
28 | class PowerControl:
29 | _shut_down_cmd_sequence = OrderedDict(
30 | [
31 | ('poweroff', ''),
32 | ('shutdown', ' -h now'),
33 | ('systemctl', ' poweroff'),
34 | ('init', ' 0'),
35 | ]
36 | )
37 |
38 | _reboot_cmd_sequence = OrderedDict(
39 | [
40 | ('reboot', ''),
41 | ('shutdown', ' -r now'),
42 | ('systemctl', ' reboot'),
43 | ('init', ' 6'),
44 | ]
45 | )
46 |
47 | @staticmethod
48 | def _find_prog(prog: str) -> str:
49 | return utils.call_subprocess(['which', prog])
50 |
51 | @classmethod
52 | def _exec_prog(cls, prog: str, args: str = '') -> bool:
53 | p = cls._find_prog(prog)
54 | logging.info('executing "%s"' % p)
55 | return os.system(p + args) == 0
56 |
57 | @classmethod
58 | def _run_procedure(cls, prog_sequence: Dict[str, str], log_msg: str) -> bool:
59 | logging.info(log_msg)
60 |
61 | for prog, args in prog_sequence.items():
62 | try:
63 | return cls._exec_prog(prog, args)
64 | except subprocess.CalledProcessError: # program not found
65 | continue
66 | else:
67 | return False
68 |
69 | @classmethod
70 | def shut_down(cls) -> bool:
71 | return cls._run_procedure(cls._shut_down_cmd_sequence, 'shutting down')
72 |
73 | @classmethod
74 | def reboot(cls) -> bool:
75 | return cls._run_procedure(cls._reboot_cmd_sequence, 'rebooting')
76 |
--------------------------------------------------------------------------------
/motioneye/controls/tzctl.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import hashlib
18 | import logging
19 | import os
20 |
21 | from motioneye import settings, utils
22 | from motioneye.config import additional_config
23 |
24 | LOCAL_TIME_FILE = settings.LOCAL_TIME_FILE # @UndefinedVariable
25 |
26 |
27 | def get_time_zone():
28 | return _get_time_zone_symlink() or _get_time_zone_md5() or 'UTC'
29 |
30 |
31 | def _get_time_zone_symlink():
32 | f = settings.LOCAL_TIME_FILE
33 | if not f:
34 | return None
35 |
36 | for i in range(8): # recursively follow the symlinks @UnusedVariable
37 | try:
38 | f = os.readlink(f)
39 |
40 | except OSError:
41 | break
42 |
43 | if f and f.startswith('/usr/share/zoneinfo/'):
44 | f = f[20:]
45 |
46 | else:
47 | f = None
48 |
49 | time_zone = f or None
50 | if time_zone:
51 | logging.debug('found time zone by symlink method: %s' % time_zone)
52 |
53 | return time_zone
54 |
55 |
56 | def _get_time_zone_md5():
57 | if settings.LOCAL_TIME_FILE:
58 | return None
59 |
60 | try:
61 | output = utils.call_subprocess(
62 | 'find * -type f | xargs md5sum', shell=True, cwd='/usr/share/zoneinfo'
63 | )
64 |
65 | except Exception as e:
66 | logging.error('getting md5 of zoneinfo files failed: %s' % e)
67 |
68 | return None
69 |
70 | lines = [l for l in output.split('\n') if l]
71 | lines = [l.split(None, 1) for l in lines]
72 | time_zone_by_md5 = dict(lines)
73 |
74 | try:
75 | with open(settings.LOCAL_TIME_FILE, 'rb') as f:
76 | data = f.read()
77 |
78 | except Exception as e:
79 | logging.error('failed to read local time file: %s' % e)
80 |
81 | return None
82 |
83 | md5 = hashlib.md5(data).hexdigest()
84 | time_zone = time_zone_by_md5.get(md5)
85 |
86 | if time_zone:
87 | logging.debug('found time zone by md5 method: %s' % time_zone)
88 |
89 | return time_zone
90 |
91 |
92 | def _set_time_zone(time_zone):
93 | time_zone = time_zone or 'UTC'
94 |
95 | zoneinfo_file = '/usr/share/zoneinfo/' + time_zone
96 | if not os.path.exists(zoneinfo_file):
97 | logging.error('%s file does not exist' % zoneinfo_file)
98 |
99 | return False
100 |
101 | logging.debug(f'linking "{settings.LOCAL_TIME_FILE}" to "{zoneinfo_file}"')
102 |
103 | try:
104 | os.remove(settings.LOCAL_TIME_FILE)
105 |
106 | except:
107 | pass # nevermind
108 |
109 | try:
110 | os.symlink(zoneinfo_file, settings.LOCAL_TIME_FILE)
111 |
112 | return True
113 |
114 | except Exception as e:
115 | logging.error(
116 | f'failed to link "{settings.LOCAL_TIME_FILE}" to "{zoneinfo_file}": {e}'
117 | )
118 |
119 | return False
120 |
121 |
122 | @additional_config
123 | def timeZone():
124 | if not LOCAL_TIME_FILE:
125 | return
126 |
127 | import pytz
128 |
129 | timezones = pytz.common_timezones
130 |
131 | return {
132 | 'label': 'Time Zone',
133 | 'description': 'selecting the right timezone assures a correct timestamp displayed on pictures and movies',
134 | 'type': 'choices',
135 | 'choices': [(t, t) for t in timezones],
136 | 'section': 'general',
137 | 'reboot': True,
138 | 'get': get_time_zone,
139 | 'set': _set_time_zone,
140 | }
141 |
--------------------------------------------------------------------------------
/motioneye/controls/v4l2ctl.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import fcntl
18 | import logging
19 | import os.path
20 | import re
21 | import stat
22 | import subprocess
23 | import time
24 | from shlex import quote
25 |
26 | from motioneye import utils
27 |
28 | _resolutions_cache = {}
29 | _ctrls_cache = {}
30 | _ctrl_values_cache = {}
31 |
32 | _DEV_V4L_BY_ID = '/dev/v4l/by-id/'
33 | _V4L2_TIMEOUT = 10
34 |
35 |
36 | def find_v4l2_ctl():
37 | try:
38 | return utils.call_subprocess(['which', 'v4l2-ctl'])
39 |
40 | except subprocess.CalledProcessError: # not found
41 | return None
42 |
43 |
44 | def list_devices():
45 | global _resolutions_cache, _ctrls_cache, _ctrl_values_cache
46 |
47 | logging.debug('listing V4L2 devices')
48 | output = b''
49 |
50 | try:
51 | output = utils.call_subprocess(
52 | ['v4l2-ctl', '--list-devices'], stderr=subprocess.STDOUT
53 | )
54 |
55 | except:
56 | logging.debug(f'v4l2-ctl error: {output}')
57 |
58 | name = None
59 | devices = []
60 | output = utils.make_str(output)
61 | for line in output.split('\n'):
62 | if line.startswith('\t'):
63 | device = line.strip()
64 | persistent_device = find_persistent_device(device)
65 | devices.append((device, persistent_device, name))
66 |
67 | logging.debug(f'found device {name}: {device}, {persistent_device}')
68 |
69 | else:
70 | name = line.split('(')[0].strip()
71 |
72 | # clear the cache
73 | _resolutions_cache = {}
74 | _ctrls_cache = {}
75 | _ctrl_values_cache = {}
76 |
77 | return devices
78 |
79 |
80 | def list_resolutions(device):
81 | from motioneye import motionctl
82 |
83 | device = utils.make_str(device)
84 |
85 | if device in _resolutions_cache:
86 | return _resolutions_cache[device]
87 |
88 | logging.debug(f'listing resolutions of device {device}...')
89 |
90 | resolutions = set()
91 | output = b''
92 | started = time.time()
93 | cmd = f"v4l2-ctl -d {quote(device)} --list-formats-ext | grep -vi stepwise | grep -oE '[0-9]+x[0-9]+' || true"
94 | logging.debug(f'running command "{cmd}"')
95 |
96 | try:
97 | output = utils.call_subprocess(cmd, shell=True, stderr=utils.DEV_NULL)
98 | except:
99 | logging.error(f'failed to list resolutions of device "{device}"')
100 |
101 | output = utils.make_str(output)
102 |
103 | for pair in output.split('\n'):
104 | pair = pair.strip()
105 | if not pair:
106 | continue
107 |
108 | width, height = pair.split('x')
109 | width = int(width)
110 | height = int(height)
111 |
112 | if (width, height) in resolutions:
113 | continue # duplicate resolution
114 |
115 | if width < 96 or height < 96: # some reasonable minimal values
116 | continue
117 |
118 | if not motionctl.resolution_is_valid(width, height):
119 | continue
120 |
121 | resolutions.add((width, height))
122 |
123 | logging.debug(f'found resolution {width}x{height} for device {device}')
124 |
125 | if not resolutions:
126 | logging.debug(f'no resolutions found for device {device}, using common values')
127 |
128 | # no resolution returned by v4l2-ctl call, add common default resolutions
129 | resolutions = utils.COMMON_RESOLUTIONS
130 | resolutions = [r for r in resolutions if motionctl.resolution_is_valid(*r)]
131 |
132 | resolutions = list(sorted(resolutions, key=lambda r: (r[0], r[1])))
133 | _resolutions_cache[device] = resolutions
134 |
135 | return resolutions
136 |
137 |
138 | def device_present(device):
139 | device = utils.make_str(device)
140 |
141 | try:
142 | st = os.stat(device)
143 | return stat.S_ISCHR(st.st_mode)
144 |
145 | except:
146 | return False
147 |
148 |
149 | def find_persistent_device(device):
150 | device = utils.make_str(device)
151 |
152 | try:
153 | devs_by_id = os.listdir(_DEV_V4L_BY_ID)
154 |
155 | except OSError:
156 | return device
157 |
158 | for p in devs_by_id:
159 | p = os.path.join(_DEV_V4L_BY_ID, p)
160 | if os.path.realpath(p) == device:
161 | return p
162 |
163 | return device
164 |
165 |
166 | def list_ctrls(device):
167 | device = utils.make_str(device)
168 |
169 | if device in _ctrls_cache:
170 | return _ctrls_cache[device]
171 |
172 | output = b''
173 | started = time.time()
174 | cmd = ['v4l2-ctl', '-d', device, '--list-ctrls']
175 | logging.debug(f'running command "{" ".join(cmd)}"')
176 |
177 | try:
178 | output = utils.call_subprocess(cmd, stderr=subprocess.STDOUT)
179 | except:
180 | logging.error(f'failed to list controls of device "{device}"')
181 |
182 | controls = {}
183 | logging.debug(f'command output: "{output}"')
184 | output = utils.make_str(output)
185 | for line in output.split('\n'):
186 | if not line:
187 | continue
188 |
189 | match = re.match(r'^\s*(\w+)\s+([a-f0-9x\s]+)?\(\w+\)\s*:\s*(.+)\s*', line)
190 | if not match:
191 | continue
192 |
193 | (control, _, properties) = match.groups()
194 | properties = dict(
195 | [v.split('=', 1) for v in properties.split(' ') if v.count('=')]
196 | )
197 | controls[control] = properties
198 |
199 | _ctrls_cache[device] = controls
200 |
201 | return controls
202 |
--------------------------------------------------------------------------------
/motioneye/extra/linux_init:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if (( UID )); then
4 | echo 'ERROR: Root permissions required. Please run this command as root user, via "sudo" or "su". Aborting...'
5 | exit 1
6 | fi
7 |
8 | # Process CLI arguments
9 | SKIP_SYSTEMD=0
10 | SKIP_APT_UPDATE=0
11 | while (( $# )); do
12 | case "$1" in
13 | '--skip-systemd') SKIP_SYSTEMD=1;;
14 | '--skip-apt-update') SKIP_APT_UPDATE=1;;
15 | *) echo "ERROR: Invalid command-line argument \"$1\". Aborting..."; exit 1;;
16 | esac
17 | shift
18 | done
19 |
20 | # Install dependencies
21 | if command -v apt-get > /dev/null; then
22 | echo 'INFO: APT-based distribution detected, installing motionEye dependencies'
23 | (( SKIP_APT_UPDATE )) || apt-get update
24 | # If matching package can be found, install from motion project GitHub releases
25 | MOTION='motion'
26 | DISTRO=''
27 | [[ -f '/etc/os-release' ]] && DISTRO=$(sed -n '/^VERSION_CODENAME=/{s/^[^=]*=//p;q}' /etc/os-release)
28 | if [[ $DISTRO ]]; then
29 | ARCH=$(dpkg --print-architecture)
30 | # On ARMv6 Raspberry Pi models, download armv6hf package
31 | PI=''
32 | [[ ${ARCH} == 'armhf' && $(uname -m) == 'armv6l' ]] && PI='pi_'
33 | echo "INFO: ${DISTRO^} on ${ARCH} detected, checking for latest motion package from GitHub releases"
34 | command -v curl > /dev/null || DEBIAN_FRONTEND="noninteractive" apt-get -y --no-install-recommends install curl
35 | URL=$(curl -sSfL 'https://api.github.com/repos/Motion-Project/motion/releases' | awk -F\" "/browser_download_url.*${PI}${DISTRO}_motion_.*_${ARCH}.deb/{print \$4}" | head -1)
36 | if [[ ${URL} ]]
37 | then
38 | echo "INFO: Matching package found, downloading: ${URL}"
39 | if curl -fLo /tmp/motion.deb "${URL}"
40 | then
41 | MOTION='/tmp/motion.deb'
42 | else
43 | echo 'WARNING: Download failed, installing (older) motion package from APT repository instead'
44 | fi
45 | else
46 | echo "WARNING: No motion package found for ${DISTRO^} on ${ARCH}, installing from APT repository instead"
47 | fi
48 | else
49 | echo 'WARNING: Distribution version could not be detected, installing motion from APT repository'
50 | fi
51 | DEBIAN_FRONTEND="noninteractive" apt-get -y --no-install-recommends install "${MOTION}" v4l-utils ffmpeg curl
52 | rm -f motion.deb
53 | elif command -v yum > /dev/null; then
54 | echo 'INFO: YUM-based distribution detected, installing motionEye dependencies'
55 | yum -y install motion v4l-utils ffmpeg curl
56 | else
57 | echo 'WARNING: This system uses neither APT nor YUM. Please install these dependencies manually:
58 | motion v4l-utils ffmpeg curl'
59 | fi
60 |
61 | # Stop and disable conflicting motion.service
62 | if systemctl -q is-active motion 2> /dev/null || systemctl -q is-enabled motion 2> /dev/null; then
63 | systemctl disable --now motion
64 | fi
65 |
66 | # Pre-create config and data dirs and install configuration files
67 | if [[ -f '/etc/motioneye/motioneye.conf' ]]
68 | then
69 | # Update PID file directory if the default from old motionEye is still present: https://github.com/motioneye-project/motioneye/issues/2657
70 | grep -q '^run_path /var/run$' /etc/motioneye/motioneye.conf && sed -i '\|^run_path /var/run$|c\run_path /run/motioneye' /etc/motioneye/motioneye.conf
71 | else
72 | [[ -d '/etc/motioneye' ]] || mkdir /etc/motioneye
73 | cp extra/motioneye.conf.sample /etc/motioneye/motioneye.conf
74 | fi
75 | chown -R motion:motion /etc/motioneye
76 |
77 | (( SKIP_SYSTEMD )) && exit 0
78 |
79 | # Install service
80 | cp extra/motioneye.systemd /etc/systemd/system/motioneye.service
81 | # - Update meyectl path if expected /usr/local/bin/meyectl does not exist, found on Arch Linux: https://github.com/motioneye-project/motioneye/issues/3005
82 | if [[ ! -f '/usr/local/bin/meyectl' ]]
83 | then
84 | meyectl_path=$(command -v meyectl)
85 | if [[ $meyectl_path ]]
86 | then
87 | echo "Using $meyectl_path for systemd service"
88 | sed -i "s|^ExecStart=/usr/local/bin/meyectl|ExecStart=$meyectl_path|" /etc/systemd/system/motioneye.service
89 | else
90 | echo 'ERROR: meyectl executable has not been found. systemd service will fail to start. Please check your motionEye installation.'
91 | fi
92 | fi
93 | systemctl daemon-reload
94 | systemctl enable --now motioneye
95 |
--------------------------------------------------------------------------------
/motioneye/extra/motioneye.conf.sample:
--------------------------------------------------------------------------------
1 | # path to the configuration directory (must be writable by motionEye)
2 | conf_path /etc/motioneye
3 |
4 | # path to the directory where pid files go (must be writable by motionEye)
5 | run_path /run/motioneye
6 |
7 | # path to the directory where log files go (must be writable by motionEye)
8 | log_path /var/log/motioneye
9 |
10 | # default output path for media files (must be writable by motionEye)
11 | media_path /var/lib/motioneye
12 |
13 | # the log level (use quiet, error, warning, info or debug)
14 | log_level info
15 |
16 | # the IP address to listen on
17 | # (0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
18 | listen 0.0.0.0
19 |
20 | # the TCP port to listen on
21 | port 8765
22 |
23 | # path to the motion binary to use (automatically detected if commented)
24 | #motion_binary /usr/bin/motion
25 |
26 | # whether motion HTTP control interface listens on
27 | # localhost or on all interfaces
28 | motion_control_localhost true
29 |
30 | # the TCP port that motion HTTP control interface listens on
31 | motion_control_port 7999
32 |
33 | # interval in seconds at which motionEye checks if motion is running
34 | motion_check_interval 10
35 |
36 | # whether to restart the motion daemon when an error occurs while communicating with it
37 | motion_restart_on_errors false
38 |
39 | # interval in seconds at which motionEye checks the SMB mounts
40 | mount_check_interval 300
41 |
42 | # interval in seconds at which the janitor is called
43 | # to remove old pictures and movies
44 | cleanup_interval 43200
45 |
46 | # timeout in seconds to wait for response from a remote motionEye server
47 | remote_request_timeout 10
48 |
49 | # timeout in seconds to wait for mjpg data from the motion daemon
50 | mjpg_client_timeout 10
51 |
52 | # timeout in seconds after which an idle mjpg client is removed
53 | # (set to 0 to disable)
54 | mjpg_client_idle_timeout 10
55 |
56 | # enable SMB shares (requires motionEye to run as root and cifs-utils installed)
57 | smb_shares false
58 |
59 | # the directory where the SMB mount points will be created
60 | smb_mount_root /media
61 |
62 | # path to the wpa_supplicant.conf file
63 | # (enable this to configure wifi settings from the UI)
64 | #wpa_supplicant_conf /etc/wpa_supplicant.conf
65 |
66 | # path to the localtime file
67 | # (enable this to configure the system time zone from the UI)
68 | #local_time_file /etc/localtime
69 |
70 | # enables shutdown and rebooting after changing system settings
71 | # (such as wifi settings or time zone)
72 | enable_reboot false
73 |
74 | # timeout in seconds to use when talking to the SMTP server
75 | smtp_timeout 60
76 |
77 | # timeout in seconds to wait for media files list
78 | list_media_timeout 120
79 |
80 | # timeout in seconds to wait for media files list, when sending emails
81 | list_media_timeout_email 10
82 |
83 | # timeout in seconds to wait for media files list, when sending a telegram
84 | list_media_timeout_telegram 10
85 |
86 | # timeout in seconds to wait for zip file creation
87 | zip_timeout 500
88 |
89 | # timeout in seconds to wait for timelapse creation
90 | timelapse_timeout 500
91 |
92 | # enable adding and removing cameras from UI
93 | add_remove_cameras true
94 |
95 | # enables HTTP basic authentication scheme (in addition to, not instead of the signature mechanism)
96 | http_basic_auth false
97 |
98 | # overrides the hostname (useful if motionEye runs behind a reverse proxy)
99 | #server_name motionEye
100 |
--------------------------------------------------------------------------------
/motioneye/extra/motioneye.systemd:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=motionEye Server
3 | After=network.target local-fs.target remote-fs.target
4 |
5 | [Service]
6 | User=motion
7 | RuntimeDirectory=motioneye
8 | LogsDirectory=motioneye
9 | StateDirectory=motioneye
10 | ExecStart=/usr/local/bin/meyectl startserver -c /etc/motioneye/motioneye.conf
11 | Restart=on-abort
12 |
13 | [Install]
14 | WantedBy=multi-user.target
15 |
--------------------------------------------------------------------------------
/motioneye/extra/motioneye.sysv:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 | #
3 | # /etc/init.d/motioneye: Start the motionEye server
4 | #
5 | ### BEGIN INIT INFO
6 | # Provides: motioneye
7 | # Required-Start: $local_fs $syslog $remote_fs $network
8 | # Required-Stop: $remote_fs
9 | # Default-Start: 2 3 4 5
10 | # Default-Stop: 0 1 6
11 | # Short-Description: Start the motionEye server
12 | # Description: Start the motionEye server
13 | ### END INIT INFO
14 |
15 | NAME='motioneye'
16 | PATH='/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin'
17 | DAEMON='/usr/local/bin/meyectl'
18 | PIDFILE="/run/$NAME.pid"
19 | DESC='motionEye server'
20 | USER='root'
21 | OPTIONS='startserver -c /etc/motioneye/motioneye.conf -l -b'
22 |
23 | . /lib/lsb/init-functions
24 |
25 | test -x "$DAEMON" || exit 0
26 |
27 | RET=0
28 |
29 | case "$1" in
30 | start)
31 | log_daemon_msg "Starting $DESC"
32 | # shellcheck disable=SC2086
33 | if start-stop-daemon --start --oknodo --exec "$DAEMON" --chuid "$USER" -- $OPTIONS; then
34 | log_end_msg 0
35 | else
36 | log_end_msg 1
37 | RET=1
38 | fi
39 | ;;
40 |
41 | stop)
42 | log_daemon_msg "Stopping $DESC"
43 | if start-stop-daemon --stop --oknodo --pidfile "$PIDFILE" --retry 5; then
44 | log_end_msg 0
45 | else
46 | log_end_msg 1
47 | RET=1
48 | fi
49 | ;;
50 |
51 | restart|force-reload)
52 | log_action_begin_msg "Restarting $DESC"
53 | if "$0" stop && "$0" start; then
54 | log_action_end_msg 0
55 | else
56 | log_action_cont_msg '(failed)'
57 | RET=1
58 | fi
59 | ;;
60 |
61 | status)
62 | status_of_proc "$DAEMON" "$NAME"
63 | ;;
64 |
65 | *)
66 | echo "Usage: /etc/init.d/$NAME {start|stop|restart|status}"
67 | RET=1
68 | ;;
69 | esac
70 |
71 | exit "$RET"
72 |
--------------------------------------------------------------------------------
/motioneye/handlers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/handlers/__init__.py
--------------------------------------------------------------------------------
/motioneye/handlers/action.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import datetime
19 | import logging
20 | import os
21 | import subprocess
22 |
23 | from tornado.ioloop import IOLoop
24 | from tornado.web import HTTPError
25 |
26 | from motioneye import config, motionctl, remote, utils
27 | from motioneye.handlers.base import BaseHandler
28 |
29 | __all__ = ('ActionHandler',)
30 |
31 |
32 | class ActionHandler(BaseHandler):
33 | async def post(self, camera_id, action):
34 | camera_id = int(camera_id)
35 | if camera_id not in config.get_camera_ids():
36 | raise HTTPError(404, 'no such camera')
37 |
38 | local_config = config.get_camera(camera_id)
39 | if utils.is_remote_camera(local_config):
40 | resp = await remote.exec_action(local_config, action)
41 | if resp.error:
42 | msg = (
43 | 'Failed to execute action on remote camera at {url}: {msg}.'.format(
44 | url=remote.pretty_camera_url(local_config), msg=resp.error
45 | )
46 | )
47 |
48 | return self.finish_json({'error': msg})
49 |
50 | return self.finish_json()
51 |
52 | if action == 'snapshot':
53 | logging.debug('executing snapshot action for camera with id %s' % camera_id)
54 | await self.snapshot(camera_id)
55 | return
56 |
57 | elif action == 'record_start':
58 | logging.debug(
59 | 'executing record_start action for camera with id %s' % camera_id
60 | )
61 | return self.record_start(camera_id)
62 |
63 | elif action == 'record_stop':
64 | logging.debug(
65 | 'executing record_stop action for camera with id %s' % camera_id
66 | )
67 | return self.record_stop(camera_id)
68 |
69 | action_commands = config.get_action_commands(local_config)
70 | command = action_commands.get(action)
71 | if not command:
72 | raise HTTPError(400, 'unknown action')
73 |
74 | logging.debug(
75 | f'executing {action} action for camera with id {camera_id}: "{command}"'
76 | )
77 | self.run_command_bg(command)
78 |
79 | def run_command_bg(self, command):
80 | self.p = subprocess.Popen(
81 | command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE
82 | )
83 | self.command = command
84 |
85 | self.io_loop = IOLoop.current()
86 | self.io_loop.add_timeout(
87 | datetime.timedelta(milliseconds=100), self.check_command
88 | )
89 |
90 | def check_command(self):
91 | exit_status = self.p.poll()
92 | if exit_status is not None:
93 | output = self.p.stdout.read()
94 | lines = output.decode('utf-8').split('\n')
95 | if not lines[-1]:
96 | lines = lines[:-1]
97 | command = os.path.basename(self.command)
98 | if exit_status:
99 | logging.warning(
100 | f'{command}: command has finished with non-zero exit status: {exit_status}'
101 | )
102 | for line in lines:
103 | logging.warning(f'{command}: {line}')
104 |
105 | else:
106 | logging.debug('%s: command has finished' % command)
107 | for line in lines:
108 | logging.debug(f'{command}: {line}')
109 |
110 | return self.finish_json({'status': exit_status})
111 |
112 | else:
113 | self.io_loop.add_timeout(
114 | datetime.timedelta(milliseconds=100), self.check_command
115 | )
116 |
117 | async def snapshot(self, camera_id):
118 | await motionctl.take_snapshot(camera_id)
119 | return self.finish_json({})
120 |
121 | def record_start(self, camera_id):
122 | return self.finish_json({})
123 |
124 | def record_stop(self, camera_id):
125 | return self.finish_json({})
126 |
--------------------------------------------------------------------------------
/motioneye/handlers/log.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import logging
19 | import os
20 |
21 | from tornado.web import HTTPError
22 |
23 | from motioneye import settings, utils
24 | from motioneye.handlers.base import BaseHandler
25 |
26 | __all__ = ('LogHandler',)
27 |
28 |
29 | class LogHandler(BaseHandler):
30 | LOGS = {
31 | 'motion': (os.path.join(settings.LOG_PATH, 'motion.log'), 'motion.log'),
32 | }
33 |
34 | @BaseHandler.auth(admin=True)
35 | def get(self, name):
36 | log = self.LOGS.get(name)
37 | if log is None:
38 | raise HTTPError(404, 'no such log')
39 |
40 | (path, filename) = log
41 |
42 | self.set_header('Content-Type', 'text/plain')
43 | self.set_header('Content-Disposition', 'attachment; filename=' + filename + ';')
44 |
45 | if path.startswith('/'): # an actual path
46 | logging.debug(f'serving log file "{filename}" from "{path}"')
47 |
48 | with open(path) as f:
49 | self.finish(f.read())
50 |
51 | else: # a command to execute
52 | logging.debug(f'serving log file "{filename}" from command "{path}"')
53 |
54 | try:
55 | output = utils.call_subprocess(path.split())
56 |
57 | except Exception as e:
58 | output = 'failed to execute command: %s' % e
59 |
60 | self.finish(output)
61 |
--------------------------------------------------------------------------------
/motioneye/handlers/login.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | from motioneye.handlers.base import BaseHandler
19 |
20 | __all__ = ('LoginHandler',)
21 |
22 |
23 | # this will only trigger the login mechanism on the client side, if required
24 | class LoginHandler(BaseHandler):
25 | @BaseHandler.auth()
26 | def get(self):
27 | self.finish_json()
28 |
29 | def post(self):
30 | self.set_header('Content-Type', 'text/html')
31 | self.finish()
32 |
--------------------------------------------------------------------------------
/motioneye/handlers/main.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | from motioneye import config, motionctl, settings, update, utils
19 | from motioneye.handlers.base import BaseHandler
20 |
21 | __all__ = ('MainHandler',)
22 |
23 |
24 | class MainHandler(BaseHandler):
25 | def get(self):
26 | # additional config
27 | main_sections = config.get_additional_structure(camera=False, separators=True)[
28 | 0
29 | ]
30 | camera_sections = config.get_additional_structure(camera=True, separators=True)[
31 | 0
32 | ]
33 |
34 | motion_info = motionctl.find_motion()
35 | os_version = update.get_os_version()
36 |
37 | self.render(
38 | 'main.html',
39 | frame=False,
40 | motion_version=motion_info[1] if motion_info else '(none)',
41 | os_version=' '.join(os_version),
42 | enable_update=settings.ENABLE_UPDATE,
43 | enable_reboot=settings.ENABLE_REBOOT,
44 | add_remove_cameras=settings.ADD_REMOVE_CAMERAS,
45 | main_sections=main_sections,
46 | camera_sections=camera_sections,
47 | hostname=settings.SERVER_NAME,
48 | title=self.get_argument('title', None),
49 | admin_username=config.get_main().get('@admin_username'),
50 | has_h264_omx_support=motionctl.has_h264_omx_support(),
51 | has_h264_v4l2m2m_support=motionctl.has_h264_v4l2m2m_support(),
52 | has_h264_nvenc_support=motionctl.has_h264_nvenc_support(),
53 | has_h264_nvmpi_support=motionctl.has_h264_nvmpi_support(),
54 | has_hevc_nvenc_support=motionctl.has_hevc_nvenc_support(),
55 | has_hevc_nvmpi_support=motionctl.has_hevc_nvmpi_support(),
56 | has_h264_qsv_support=motionctl.has_h264_qsv_support(),
57 | has_hevc_qsv_support=motionctl.has_hevc_qsv_support(),
58 | has_motion=bool(motionctl.find_motion()[0]),
59 | mask_width=utils.MASK_WIDTH,
60 | )
61 |
--------------------------------------------------------------------------------
/motioneye/handlers/movie_playback.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import logging
19 | import os
20 | import tempfile
21 | import time
22 |
23 | from tornado.web import HTTPError, StaticFileHandler
24 |
25 | from motioneye import config, mediafiles, remote, utils
26 | from motioneye.handlers.base import BaseHandler
27 |
28 | __all__ = ('MoviePlaybackHandler', 'MovieDownloadHandler')
29 |
30 |
31 | # support fetching movies with authentication
32 | class MoviePlaybackHandler(StaticFileHandler, BaseHandler):
33 | tmpdir = tempfile.gettempdir() + '/MotionEye'
34 | if not os.path.exists(tmpdir):
35 | os.mkdir(tmpdir)
36 |
37 | @BaseHandler.auth()
38 | async def get(self, camera_id, filename=None, include_body=True):
39 | logging.debug(
40 | 'downloading movie {filename} of camera {id}'.format(
41 | filename=filename, id=camera_id
42 | )
43 | )
44 |
45 | self.pretty_filename = os.path.basename(filename)
46 |
47 | if camera_id is not None:
48 | camera_id = int(camera_id)
49 | if camera_id not in config.get_camera_ids():
50 | raise HTTPError(404, 'no such camera')
51 |
52 | camera_config = config.get_camera(camera_id)
53 |
54 | if utils.is_local_motion_camera(camera_config):
55 | filename = mediafiles.get_media_path(camera_config, filename, 'movie')
56 | self.pretty_filename = (
57 | camera_config['camera_name'] + '_' + self.pretty_filename
58 | )
59 | await StaticFileHandler.get(self, filename, include_body=include_body)
60 | return
61 |
62 | elif utils.is_remote_camera(camera_config):
63 | # we will cache the movie since it takes a while to fetch from the remote camera
64 | # and we may be going to play it back in the browser, which will fetch the video in chunks
65 | tmpfile = self.tmpdir + '/' + self.pretty_filename
66 | if os.path.isfile(tmpfile):
67 | # have a cached copy, update the timestamp so it's not flushed
68 | import time
69 |
70 | mtime = os.stat(tmpfile).st_mtime
71 | os.utime(tmpfile, (time.time(), mtime))
72 | await StaticFileHandler.get(self, tmpfile, include_body=include_body)
73 | return
74 |
75 | resp = await remote.get_media_content(
76 | camera_config, filename, media_type='movie'
77 | )
78 | if resp.error:
79 | return self.finish_json(
80 | {
81 | 'error': 'Failed to download movie from {url}: {msg}.'.format(
82 | url=remote.pretty_camera_url(camera_config), msg=resp.error
83 | )
84 | }
85 | )
86 |
87 | # check if the file has been created by another request while we were fetching the movie
88 | if not os.path.isfile(tmpfile):
89 | tmp = open(tmpfile, 'wb')
90 | tmp.write(resp.result)
91 | tmp.close()
92 |
93 | await StaticFileHandler.get(self, tmpfile, include_body=include_body)
94 | return
95 |
96 | else: # assuming simple mjpeg camera
97 | raise HTTPError(400, 'unknown operation')
98 |
99 | def on_finish(self):
100 | # delete any cached file older than an hour
101 | stale_time = time.time() - (60 * 60)
102 | try:
103 | for f in os.listdir(self.tmpdir):
104 | f = os.path.join(self.tmpdir, f)
105 | if os.path.isfile(f) and os.stat(f).st_atime <= stale_time:
106 | os.remove(f)
107 | except:
108 | logging.error('could not delete temp file', exc_info=True)
109 | pass
110 |
111 | def get_absolute_path(self, root, path):
112 | return path
113 |
114 | def validate_absolute_path(self, root, absolute_path):
115 | return absolute_path
116 |
117 |
118 | class MovieDownloadHandler(MoviePlaybackHandler):
119 | def set_extra_headers(self, filename):
120 | if self.get_status() in (200, 304):
121 | self.set_header(
122 | 'Content-Disposition',
123 | 'attachment; filename=' + self.pretty_filename + ';',
124 | )
125 | self.set_header('Expires', '0')
126 |
--------------------------------------------------------------------------------
/motioneye/handlers/power.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import datetime
19 |
20 | from tornado.ioloop import IOLoop
21 |
22 | from motioneye.controls.powerctl import PowerControl
23 | from motioneye.handlers.base import BaseHandler
24 |
25 | __all__ = ('PowerHandler',)
26 |
27 |
28 | class PowerHandler(BaseHandler):
29 | @BaseHandler.auth(admin=True)
30 | def post(self, op):
31 | if op == 'shutdown':
32 | self.shut_down()
33 |
34 | elif op == 'reboot':
35 | self.reboot()
36 |
37 | def shut_down(self):
38 | io_loop = IOLoop.current()
39 | io_loop.add_timeout(datetime.timedelta(seconds=2), PowerControl.shut_down)
40 |
41 | def reboot(self):
42 | io_loop = IOLoop.current()
43 | io_loop.add_timeout(datetime.timedelta(seconds=2), PowerControl.reboot)
44 |
--------------------------------------------------------------------------------
/motioneye/handlers/prefs.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import json
19 | import logging
20 |
21 | from motioneye.handlers.base import BaseHandler
22 |
23 | __all__ = ('PrefsHandler',)
24 |
25 |
26 | class PrefsHandler(BaseHandler):
27 | def get(self, key=None):
28 | self.finish_json(self.get_pref(key))
29 |
30 | def post(self, key=None):
31 | try:
32 | value = json.loads(self.request.body)
33 |
34 | except Exception as e:
35 | logging.error('could not decode json: %s' % e)
36 |
37 | raise
38 |
39 | self.set_pref(key, value)
40 |
--------------------------------------------------------------------------------
/motioneye/handlers/relay_event.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import logging
19 |
20 | from motioneye import config, mediafiles, motionctl, tasks, uploadservices, utils
21 | from motioneye.handlers.base import BaseHandler
22 |
23 | __all__ = ('RelayEventHandler',)
24 |
25 |
26 | class RelayEventHandler(BaseHandler):
27 | @BaseHandler.auth(admin=True)
28 | def post(self):
29 | event = self.get_argument('event')
30 | motion_camera_id = int(self.get_argument('motion_camera_id'))
31 |
32 | camera_id = motionctl.motion_camera_id_to_camera_id(motion_camera_id)
33 | if camera_id is None:
34 | logging.debug(
35 | 'ignoring event for unknown motion camera id %s' % motion_camera_id
36 | )
37 | return self.finish_json()
38 |
39 | else:
40 | logging.debug(
41 | 'received relayed event {event} for motion camera id {id} (camera id {cid})'.format(
42 | event=event, id=motion_camera_id, cid=camera_id
43 | )
44 | )
45 |
46 | camera_config = config.get_camera(camera_id)
47 | if not utils.is_local_motion_camera(camera_config):
48 | logging.warning(
49 | 'ignoring event for non-local camera with id %s' % camera_id
50 | )
51 | return self.finish_json()
52 |
53 | if event == 'start':
54 | if not camera_config['@motion_detection']:
55 | logging.debug(
56 | 'ignoring start event for camera with id %s and motion detection disabled'
57 | % camera_id
58 | )
59 | return self.finish_json()
60 |
61 | motionctl.set_motion_detected(camera_id, True)
62 |
63 | elif event == 'stop':
64 | motionctl.set_motion_detected(camera_id, False)
65 |
66 | elif event == 'movie_end':
67 | filename = self.get_argument('filename')
68 |
69 | # generate preview (thumbnail)
70 | tasks.add(
71 | 5,
72 | mediafiles.make_movie_preview,
73 | tag='make_movie_preview(%s)' % filename,
74 | camera_config=camera_config,
75 | full_path=filename,
76 | )
77 |
78 | # upload to external service
79 | if camera_config['@upload_enabled'] and camera_config['@upload_movie']:
80 | self.upload_media_file(filename, camera_id, camera_config)
81 |
82 | elif event == 'picture_save':
83 | filename = self.get_argument('filename')
84 |
85 | # upload to external service
86 | if camera_config['@upload_enabled'] and camera_config['@upload_picture']:
87 | self.upload_media_file(filename, camera_id, camera_config)
88 |
89 | else:
90 | logging.warning('unknown event %s' % event)
91 |
92 | self.finish_json()
93 |
94 | def upload_media_file(self, filename, camera_id, camera_config):
95 | service_name = camera_config['@upload_service']
96 |
97 | tasks.add(
98 | 5,
99 | uploadservices.upload_media_file,
100 | tag='upload_media_file(%s)' % filename,
101 | camera_id=camera_id,
102 | service_name=service_name,
103 | camera_name=camera_config['camera_name'],
104 | target_dir=camera_config['@upload_subfolders']
105 | and camera_config['target_dir'],
106 | filename=filename,
107 | )
108 |
--------------------------------------------------------------------------------
/motioneye/handlers/update.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import logging
19 | from functools import cmp_to_key
20 |
21 | from motioneye.handlers.base import BaseHandler
22 | from motioneye.update import (
23 | compare_versions,
24 | get_all_versions,
25 | get_os_version,
26 | perform_update,
27 | )
28 |
29 | __all__ = ('UpdateHandler',)
30 |
31 |
32 | class UpdateHandler(BaseHandler):
33 | @BaseHandler.auth(admin=True)
34 | def get(self):
35 | logging.debug('listing versions')
36 |
37 | versions = get_all_versions()
38 | current_version = get_os_version()[
39 | 1
40 | ] # os version is returned as (name, version) tuple
41 | recent_versions = [
42 | v for v in versions if compare_versions(v, current_version) > 0
43 | ]
44 | recent_versions.sort(key=cmp_to_key(compare_versions))
45 | update_version = recent_versions[-1] if recent_versions else None
46 |
47 | self.finish_json(
48 | {'update_version': update_version, 'current_version': current_version}
49 | )
50 |
51 | @BaseHandler.auth(admin=True)
52 | def post(self):
53 | version = self.get_argument('version')
54 |
55 | logging.debug(f'performing update to version {version}')
56 |
57 | result = perform_update(version)
58 |
59 | self.finish_json(result)
60 |
--------------------------------------------------------------------------------
/motioneye/handlers/version.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import socket
19 |
20 | from motioneye.handlers.base import BaseHandler
21 | from motioneye.motionctl import find_motion
22 | from motioneye.update import get_os_version
23 |
24 | __all__ = ('VersionHandler',)
25 |
26 |
27 | class VersionHandler(BaseHandler):
28 | def get(self):
29 | motion_info = find_motion()
30 | os_version = get_os_version()
31 |
32 | self.render(
33 | 'version.html',
34 | os_version=' '.join(os_version),
35 | motion_version=motion_info[1] if motion_info else '',
36 | hostname=socket.gethostname(),
37 | )
38 |
39 | post = get
40 |
--------------------------------------------------------------------------------
/motioneye/locale/ar/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ar/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/bn/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/bn/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/ca/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ca/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/cs/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/cs/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/de/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/de/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/el/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/el/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/en/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/en/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/es/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/es/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/fi/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/fi/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/fr/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/fr/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/hi/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/hi/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/hu/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/hu/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/it/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/it/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/ja/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ja/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/ko/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ko/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/ms/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ms/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/nb_NO/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/nb_NO/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/nl/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/nl/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/pa/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/pa/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/pl/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/pl/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/pt/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/pt/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/ro/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ro/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/ru/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ru/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/sk/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/sk/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/sv/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/sv/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/ta/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/ta/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/tr/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/tr/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/uk/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/uk/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/vi/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/vi/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/locale/zh/LC_MESSAGES/motioneye.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/locale/zh/LC_MESSAGES/motioneye.mo
--------------------------------------------------------------------------------
/motioneye/monitor.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import logging
18 | import subprocess
19 | import time
20 | import urllib.error
21 | import urllib.parse
22 | import urllib.request
23 |
24 | from motioneye import config
25 |
26 | DEFAULT_INTERVAL = 1 # seconds
27 |
28 | _monitor_info_cache_by_camera_id = {}
29 | _last_call_time_by_camera_id = {}
30 | _interval_by_camera_id = {}
31 |
32 |
33 | def get_monitor_info(camera_id):
34 | now = time.time()
35 | command = config.get_monitor_command(camera_id)
36 | if command is None:
37 | return ''
38 |
39 | monitor_info = _monitor_info_cache_by_camera_id.get(camera_id)
40 | last_call_time = _last_call_time_by_camera_id.get(camera_id, 0)
41 | interval = _interval_by_camera_id.get(camera_id, DEFAULT_INTERVAL)
42 | if monitor_info is None or now - last_call_time > interval:
43 | monitor_info, interval = _exec_monitor_command(command)
44 | monitor_info = urllib.parse.quote(monitor_info, safe='')
45 | _interval_by_camera_id[camera_id] = interval
46 | _monitor_info_cache_by_camera_id[camera_id] = monitor_info
47 | _last_call_time_by_camera_id[camera_id] = now
48 |
49 | return monitor_info
50 |
51 |
52 | def _exec_monitor_command(command):
53 | process = subprocess.Popen(
54 | [command], stdout=subprocess.PIPE, stderr=subprocess.PIPE
55 | )
56 | out, err = process.communicate()
57 |
58 | try:
59 | interval = int(err)
60 |
61 | except:
62 | interval = DEFAULT_INTERVAL
63 |
64 | out = out.strip()
65 | logging.debug(f'monitoring command "{command}" returned "{out}"')
66 |
67 | return out, interval
68 |
--------------------------------------------------------------------------------
/motioneye/motioneye_init.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Copyright (c) 2022 Jean Michault
4 | # This file is part of motionEye.
5 | #
6 | # motionEye is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import subprocess
20 | import sys
21 |
22 | import motioneye
23 |
24 |
25 | def main():
26 | cmd = f"cd '{motioneye.__path__[0]}' && extra/linux_init"
27 | for arg in sys.argv[1:]:
28 | cmd += f" '{arg}'"
29 |
30 | subprocess.run(cmd, shell=True)
31 |
32 |
33 | if __name__ == '__main__':
34 | main()
35 |
--------------------------------------------------------------------------------
/motioneye/prefs.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import json
18 | import logging
19 | import os.path
20 |
21 | from motioneye import settings
22 |
23 | _PREFS_FILE_NAME = 'prefs.json'
24 | _DEFAULT_PREFS = {
25 | 'layout_columns': 3,
26 | 'fit_frames_vertically': True,
27 | 'layout_rows': 1,
28 | 'framerate_factor': 1,
29 | 'resolution_factor': 1,
30 | }
31 |
32 | _prefs = None
33 |
34 |
35 | def _load():
36 | global _prefs
37 |
38 | _prefs = {}
39 |
40 | file_path = os.path.join(settings.CONF_PATH, _PREFS_FILE_NAME)
41 |
42 | if os.path.exists(file_path):
43 | logging.debug('loading preferences from "%s"...' % file_path)
44 |
45 | try:
46 | f = open(file_path)
47 |
48 | except Exception as e:
49 | logging.error(f'could not open preferences file "{file_path}": {e}')
50 |
51 | return
52 |
53 | try:
54 | _prefs = json.load(f)
55 |
56 | except Exception as e:
57 | logging.error(f'could not read preferences from file "{file_path}": {e}')
58 |
59 | finally:
60 | f.close()
61 |
62 | else:
63 | logging.debug(
64 | 'preferences file "%s" does not exist, using default preferences'
65 | % file_path
66 | )
67 |
68 |
69 | def _save():
70 | file_path = os.path.join(settings.CONF_PATH, _PREFS_FILE_NAME)
71 |
72 | logging.debug('saving preferences to "%s"...' % file_path)
73 |
74 | try:
75 | f = open(file_path, 'w')
76 |
77 | except Exception as e:
78 | logging.error(f'could not open preferences file "{file_path}": {e}')
79 |
80 | return
81 |
82 | try:
83 | json.dump(_prefs, f, sort_keys=True, indent=4)
84 |
85 | except Exception as e:
86 | logging.error(f'could not save preferences to file "{file_path}": {e}')
87 |
88 | finally:
89 | f.close()
90 |
91 |
92 | def get(username, key=None):
93 | if _prefs is None:
94 | _load()
95 |
96 | if key:
97 | prefs = _prefs.get(username, {}).get(key, _DEFAULT_PREFS.get(key))
98 |
99 | else:
100 | prefs = dict(_DEFAULT_PREFS)
101 | prefs.update(_prefs.get(username, {}))
102 |
103 | return prefs
104 |
105 |
106 | def set(username, key, value):
107 | if _prefs is None:
108 | _load()
109 |
110 | if key:
111 | _prefs.setdefault(username, {})[key] = value
112 |
113 | else:
114 | _prefs[username] = value
115 |
116 | _save()
117 |
--------------------------------------------------------------------------------
/motioneye/scripts/migrateconf.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [[ -z "$1" ]]; then
4 | echo "Usage: $0 "
5 | exit 1
6 | fi
7 |
8 | file=$1
9 | tmp_file="${file}.tmp"
10 | bak_file="${file}.bak"
11 |
12 | # make a backup
13 | echo "backing up ${file}"
14 | cp "${file}" "${bak_file}"
15 |
16 | function adjust_directive() {
17 | # $1 - old directive
18 | # $2 - new directive
19 |
20 | if ! grep -q "^$1" "${file}"; then
21 | return
22 | fi
23 |
24 | echo "adjusting $1 -> $2"
25 | sed -ri "s/^$1(.*)/$2\1/" "${file}"
26 | }
27 |
28 | function remove_directive() {
29 | # $1 - old directive
30 |
31 | if ! grep -q "^$1 " "${file}"; then
32 | return
33 | fi
34 |
35 | echo "removing $1"
36 |
37 | grep -vE "^$1 " "${file}" > "${tmp_file}"
38 | mv "${tmp_file}" "${file}"
39 | }
40 |
41 |
42 | # 3.x -> 4.2
43 | adjust_directive "control_authentication" "webcontrol_authentication"
44 | adjust_directive "control_html_output\s+on" "webcontrol_interface 1"
45 | adjust_directive "control_html_output\s+off" "webcontrol_interface 0"
46 | adjust_directive "control_localhost" "webcontrol_localhost"
47 | adjust_directive "control_port" "webcontrol_port"
48 | adjust_directive "gap" "event_gap"
49 | adjust_directive "jpeg_filename" "picture_filename"
50 | adjust_directive "locate\s+on" "locate_motion_mode on\nlocate_motion_style redbox"
51 | adjust_directive "locate\s+off" "locate_motion_mode off\nlocate_motion_style redbox"
52 | adjust_directive "output_all" "emulate_motion"
53 | adjust_directive "output_normal" "picture_output"
54 | adjust_directive "output_motion" "picture_output_motion"
55 | adjust_directive "thread\s+thread-" "camera camera-"
56 | adjust_directive "#thread\s+thread-" "#camera camera-"
57 | adjust_directive "webcam_localhost" "stream_localhost"
58 | adjust_directive "webcam_maxrate" "stream_maxrate"
59 | adjust_directive "webcam_motion" "stream_motion"
60 | adjust_directive "webcam_port" "stream_port"
61 | adjust_directive "webcam_quality" "stream_quality"
62 |
63 | # 4.0/4.1 -> 4.2
64 | adjust_directive "# @name" "camera_name"
65 | adjust_directive "extpipe" "movie_extpipe"
66 | adjust_directive "exif" "picture_exif"
67 | adjust_directive "ffmpeg_bps" "movie_bps"
68 | adjust_directive "ffmpeg_duplicate_frames" "movie_duplicate_frames"
69 | adjust_directive "ffmpeg_output_movies" "movie_output"
70 | adjust_directive "ffmpeg_output_debug_movies" "movie_output_motion"
71 | adjust_directive "ffmpeg_variable_bitrate" "movie_quality"
72 | adjust_directive "ffmpeg_video_codec" "movie_codec"
73 | adjust_directive "lightswitch" "lightswitch_percent"
74 | adjust_directive "max_movie_time" "movie_max_time"
75 | adjust_directive "output_pictures" "picture_output"
76 | adjust_directive "output_debug_pictures" "picture_output_motion"
77 | adjust_directive "quality" "picture_quality"
78 | adjust_directive "process_id_file" "pid_file"
79 | adjust_directive "rtsp_uses_tcp" "netcam_use_tcp"
80 | adjust_directive "text_double\s+on" "text_scale 2"
81 | adjust_directive "text_double\s+off" "text_scale 1"
82 | adjust_directive "webcontrol_html_output\s+on" "webcontrol_interface 1"
83 | adjust_directive "webcontrol_html_output\s+off" "webcontrol_interface 0"
84 |
85 | # these video controls have been removed and replaced by vid_control_params directive
86 | # user will have to reconfigure them from scratch
87 | remove_directive "brightness"
88 | remove_directive "contrast"
89 | remove_directive "hue"
90 | remove_directive "saturation"
91 |
92 |
93 | # rename thread file
94 | bn=$(basename "${file}")
95 | dn=$(dirname "${file}")
96 | if [[ ${bn} =~ thread-(.*)\.conf ]]; then
97 | mv "${file}" "${dn}/camera-${BASH_REMATCH[1]}.conf"
98 | fi
99 |
--------------------------------------------------------------------------------
/motioneye/scripts/relayevent.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | if [ -z "$3" ]; then
4 | echo "Usage: $0 [filename]"
5 | exit 1
6 | fi
7 |
8 | timeout=5
9 |
10 | motioneye_conf=$1
11 | if [ -f "$motioneye_conf" ]; then
12 | port=$(grep '^port' "$motioneye_conf" | cut -d ' ' -f 2)
13 | conf_path=$(grep '^conf_path' "$motioneye_conf" | cut -d ' ' -f 2)
14 | if [ "$conf_path" ]; then
15 | motion_conf="$conf_path/motion.conf"
16 | if [ -r "$motion_conf" ]; then
17 | username=$(grep 'admin_username' "$motion_conf" | cut -d ' ' -f 3)
18 | password=$(grep 'admin_password' "$motion_conf" | cut -d ' ' -f 3 | sed -r 's/[^][a-zA-Z0-9/?_.=&{}":, _]/-/g')
19 | fi
20 | fi
21 | fi
22 |
23 | [ "$port" ] || port='8765'
24 | [ "$username" ] || username='admin'
25 |
26 | event=$2
27 | motion_camera_id=$3
28 | filename=$4
29 |
30 | uri="/_relay_event/?_username=$username&event=$event&motion_camera_id=$motion_camera_id"
31 | data="{\"filename\": \"$filename\"}"
32 | signature=$(printf '%s' "POST:$uri:$data:$password" | sha1sum | cut -d ' ' -f 1)
33 |
34 | curl -sSfm "$timeout" -H 'Content-Type: application/json' -X POST "http://127.0.0.1:$port$uri&_signature=$signature" -d "$data" -o /dev/null
35 |
--------------------------------------------------------------------------------
/motioneye/sendtelegram.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import datetime
18 | import logging
19 | import os
20 | import re
21 | import signal
22 | import socket
23 | import time
24 |
25 | import pycurl
26 | from tornado.ioloop import IOLoop
27 |
28 | from motioneye import config, mediafiles, meyectl, motionctl, settings, utils
29 | from motioneye.controls import tzctl
30 |
31 |
32 | def send_message(api_key, chat_id, message, files):
33 | telegram_message_url = 'https://api.telegram.org/bot%s/sendMessage' % api_key
34 | telegram_photo_url = 'https://api.telegram.org/bot%s/sendPhoto' % api_key
35 | c = pycurl.Curl()
36 | c.setopt(c.POST, 1)
37 | c.setopt(c.URL, telegram_message_url)
38 | if not files:
39 | logging.info('no files')
40 | c.setopt(c.POSTFIELDS, f"chat_id={chat_id}&text={message}")
41 | c.perform()
42 | else:
43 | logging.info('files present')
44 | for f in files:
45 | c.setopt(c.URL, telegram_photo_url)
46 | # Send photos
47 | c.setopt(
48 | c.HTTPPOST,
49 | [
50 | ("chat_id", chat_id),
51 | ("caption", message),
52 | ("photo", (c.FORM_FILE, f)),
53 | ],
54 | )
55 | c.perform()
56 | c.close()
57 | logging.debug('sending telegram')
58 |
59 |
60 | def make_message(message, camera_id, moment, timespan, callback):
61 | camera_config = config.get_camera(camera_id)
62 |
63 | # we must start the IO loop for the media list subprocess polling
64 | io_loop = IOLoop.current()
65 |
66 | def on_media_files(media_files):
67 | io_loop.stop()
68 | photos = []
69 |
70 | timestamp = time.mktime(moment.timetuple())
71 | if media_files:
72 | logging.debug('got media files')
73 | media_files = [
74 | m
75 | for m in media_files.result()
76 | if abs(m['timestamp'] - timestamp) < float(timespan)
77 | ]
78 | media_files.sort(key=lambda m: m['timestamp'], reverse=True)
79 | media_files = [
80 | os.path.join(camera_config['target_dir'], re.sub('^/', '', m['path']))
81 | for m in media_files
82 | ]
83 | logging.debug('selected %d pictures' % len(media_files))
84 |
85 | format_dict = {
86 | 'camera': camera_config['camera_name'],
87 | 'hostname': socket.gethostname(),
88 | 'moment': moment.strftime('%Y-%m-%d %H:%M:%S'),
89 | }
90 |
91 | if settings.LOCAL_TIME_FILE:
92 | format_dict['timezone'] = tzctl.get_time_zone()
93 | else:
94 | format_dict['timezone'] = 'local time'
95 |
96 | logging.debug('creating telegram message')
97 |
98 | m = message % format_dict
99 |
100 | callback(m, media_files)
101 |
102 | if not timespan:
103 | return on_media_files([])
104 |
105 | logging.debug(f'waiting {float(timespan)}s for pictures to be taken')
106 | time.sleep(float(timespan)) # give motion some time to create motion pictures
107 |
108 | prefix = None
109 | picture_filename = camera_config.get('picture_filename')
110 | snapshot_filename = camera_config.get('snapshot_filename')
111 |
112 | if (
113 | (picture_filename or snapshot_filename)
114 | and not picture_filename
115 | or picture_filename.startswith('%Y-%m-%d/')
116 | and not snapshot_filename
117 | or snapshot_filename.startswith('%Y-%m-%d/')
118 | ):
119 | prefix = moment.strftime('%Y-%m-%d')
120 | logging.debug('narrowing down still images path lookup to %s' % prefix)
121 |
122 | fut = utils.cast_future(
123 | mediafiles.list_media(camera_config, media_type='picture', prefix=prefix)
124 | )
125 | fut.add_done_callback(on_media_files)
126 | io_loop.start()
127 |
128 |
129 | def parse_options(parser, args):
130 | parser.description = 'Send Telegram using bot api'
131 | parser.add_argument('api', help='telegram api key')
132 | parser.add_argument('chatid', help='telegram chat room id')
133 | parser.add_argument('motion_camera_id', help='the id of the motion camera')
134 | parser.add_argument(
135 | 'moment',
136 | help='the moment in ISO-8601 format',
137 | type=datetime.datetime.fromisoformat,
138 | )
139 | parser.add_argument('timespan', help='picture collection time span')
140 | return parser.parse_args(args)
141 |
142 |
143 | def main(parser, args):
144 | # the motion daemon overrides SIGCHLD,
145 | # so we must restore it here,
146 | # or otherwise media listing won't work
147 | signal.signal(signal.SIGCHLD, signal.SIG_DFL)
148 |
149 | options = parse_options(parser, args)
150 | meyectl.configure_logging('telegram', options.log_to_file)
151 | logging.debug(options)
152 | message = 'Motion has been detected by camera "%(camera)s/%(hostname)s" at %(moment)s (%(timezone)s).'
153 |
154 | # do not wait too long for media list,
155 | # telegram notifications are critical
156 | settings.LIST_MEDIA_TIMEOUT = settings.LIST_MEDIA_TIMEOUT_TELEGRAM
157 |
158 | camera_id = motionctl.motion_camera_id_to_camera_id(options.motion_camera_id)
159 |
160 | def on_message(message, files):
161 | try:
162 | logging.info(f'sending telegram : {message}')
163 | send_message(options.api, options.chatid, message, files or [])
164 | logging.info('telegram sent')
165 |
166 | except Exception as e:
167 | logging.error('failed to send telegram: %s' % e, exc_info=True)
168 |
169 | logging.debug('bye!')
170 |
171 | make_message(message, camera_id, options.moment, options.timespan, on_message)
172 |
--------------------------------------------------------------------------------
/motioneye/settings.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os.path
3 | import socket
4 | import sys
5 |
6 | import motioneye
7 |
8 | config_file = None
9 |
10 | # interface language
11 | lingvo = 'eo'
12 |
13 | # available languages
14 | langlist = [('en', 'English'), ('eo', 'esperanto'), ('fr', 'français')]
15 |
16 | # gettext translation
17 | traduction = None
18 |
19 | # the root directory of the project
20 | PROJECT_PATH = os.path.dirname(motioneye.__file__)
21 |
22 | # the templates directory
23 | TEMPLATE_PATH = os.path.join(PROJECT_PATH, 'templates')
24 |
25 | # the static files directory
26 | STATIC_PATH = os.path.join(PROJECT_PATH, 'static')
27 |
28 | # path to the configuration directory (must be writable by motionEye)
29 | CONF_PATH = [sys.prefix, ''][sys.prefix == '/usr'] + '/etc/motioneye'
30 |
31 | # path to the directory where pid files go (must be writable by motionEye)
32 | for d in ['/run', '/var/run', '/tmp', '/var/tmp']:
33 | if os.path.exists(d):
34 | RUN_PATH = d
35 | break
36 |
37 | else:
38 | RUN_PATH = PROJECT_PATH
39 |
40 | # path to the directory where log files go (must be writable by motionEye)
41 | for d in ['/log', '/var/log', '/tmp', '/var/tmp']:
42 | if os.path.exists(d):
43 | LOG_PATH = d
44 | break
45 |
46 | else:
47 | LOG_PATH = RUN_PATH
48 |
49 | # default output path for media files (must be writable by motionEye)
50 | MEDIA_PATH = '/var/lib/motioneye'
51 |
52 | # the log level (use FATAL, ERROR, WARNING, INFO or DEBUG)
53 | LOG_LEVEL = logging.INFO
54 |
55 | # the IP address to listen on
56 | # (0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
57 | LISTEN = '0.0.0.0'
58 |
59 | # the TCP port to listen on
60 | PORT = 8765
61 |
62 | # path to the motion binary to use (automatically detected by default)
63 | MOTION_BINARY = None
64 |
65 | # whether motion HTTP control interface listens on
66 | # localhost or on all interfaces
67 | MOTION_CONTROL_LOCALHOST = True
68 |
69 | # the TCP port that motion HTTP control interface listens on
70 | MOTION_CONTROL_PORT = 7999
71 |
72 | # interval in seconds at which motionEye checks if motion is running
73 | MOTION_CHECK_INTERVAL = 10
74 |
75 | # whether to restart the motion daemon when an error occurs while communicating with it
76 | MOTION_RESTART_ON_ERRORS = False
77 |
78 | # interval in seconds at which motionEye checks the SMB mounts
79 | MOUNT_CHECK_INTERVAL = 300
80 |
81 | # interval in seconds at which the janitor is called
82 | # to remove old pictures and movies
83 | CLEANUP_INTERVAL = 43200
84 |
85 | # timeout in seconds to wait for response from a remote motionEye server
86 | REMOTE_REQUEST_TIMEOUT = 10
87 |
88 | # timeout in seconds to wait for mjpg data from the motion daemon
89 | MJPG_CLIENT_TIMEOUT = 10
90 |
91 | # timeout in seconds after which an idle mjpg client is removed
92 | # (set to 0 to disable)
93 | MJPG_CLIENT_IDLE_TIMEOUT = 10
94 |
95 | # enable SMB shares (requires motionEye to run as root)
96 | SMB_SHARES = False
97 |
98 | # the directory where the SMB mount points will be created
99 | SMB_MOUNT_ROOT = '/media'
100 |
101 | # path to the wpa_supplicant.conf file
102 | # (enable this to configure wifi settings from the UI)
103 | WPA_SUPPLICANT_CONF = None
104 |
105 | # path to the localtime file
106 | # (enable this to configure the system time zone from the UI)
107 | LOCAL_TIME_FILE = None
108 |
109 | # enables shutdown and rebooting after changing system settings
110 | # (such as wifi settings or time zone)
111 | ENABLE_REBOOT = False
112 |
113 | # enables motionEye version update (not implemented by default)
114 | ENABLE_UPDATE = False
115 |
116 | # timeout in seconds to use when talking to the SMTP server
117 | SMTP_TIMEOUT = 60
118 |
119 | # timeout in seconds to wait for media files list
120 | LIST_MEDIA_TIMEOUT = 120
121 |
122 | # timeout in seconds to wait for media files list, when sending emails
123 | LIST_MEDIA_TIMEOUT_EMAIL = 10
124 |
125 | # timeout in seconds to wait for media files list, when sending telegrams
126 | LIST_MEDIA_TIMEOUT_TELEGRAM = 10
127 |
128 | # timeout in seconds to wait for zip file creation
129 | ZIP_TIMEOUT = 500
130 |
131 | # timeout in seconds to wait for timelapse creation
132 | TIMELAPSE_TIMEOUT = 500
133 |
134 | # enable adding and removing cameras from UI
135 | ADD_REMOVE_CAMERAS = True
136 |
137 | # enable HTTPS certificate validation
138 | VALIDATE_CERTS = True
139 |
140 | # an external program to be executed whenever a password changes;
141 | # the program will be invoked with environment variables MEYE_USERNAME and MEYE_PASSWORD
142 | PASSWORD_HOOK = None
143 |
144 | # enables HTTP basic authentication scheme (in addition to, not instead of the signature mechanism)
145 | HTTP_BASIC_AUTH = False
146 |
147 | # provides the possibility to override the hostname
148 | SERVER_NAME = socket.gethostname()
149 |
--------------------------------------------------------------------------------
/motioneye/shell.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import code
18 | import logging
19 |
20 |
21 | def parse_options(parser, args):
22 | return parser.parse_args(args)
23 |
24 |
25 | def main(parser, args):
26 | from motioneye import meyectl
27 |
28 | options = parse_options(parser, args)
29 |
30 | meyectl.configure_logging('shell', options.log_to_file)
31 | meyectl.configure_tornado()
32 |
33 | logging.debug('hello!')
34 |
35 | code.interact(local=locals())
36 |
37 | logging.debug('bye!')
38 |
--------------------------------------------------------------------------------
/motioneye/static/css/frame.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | /* basic */
4 |
5 | body {
6 | color: #dddddd;
7 | background-color: #212121;
8 | }
9 |
10 |
11 | /* camera frame */
12 |
13 | div.camera-frame {
14 | position: relative;
15 | padding: 0;
16 | margin: 0;
17 | border: 0;
18 | width: 100%;
19 | height: 100%;
20 | }
21 |
22 | div.camera-container {
23 | height: 100%;
24 | text-align: center;
25 | overflow: hidden;
26 | }
27 |
28 | img.camera {
29 | height: auto;
30 | margin: auto;
31 | }
32 |
33 | img.camera.error,
34 | img.camera.loading {
35 | height: 100% !important;
36 | }
37 |
38 | div.camera-placeholder {
39 | overflow: hidden;
40 | }
41 |
42 | div.camera-progress {
43 | cursor: default;
44 | }
45 |
--------------------------------------------------------------------------------
/motioneye/static/css/jquery.timepicker.min.css:
--------------------------------------------------------------------------------
1 | /*! https://raw.githubusercontent.com/jonthornton/jquery-timepicker/master/jquery.timepicker.min.css */
2 | .ui-timepicker-wrapper{overflow-y:auto;max-height:150px;width:6.5em;background:#fff;border:1px solid #ddd;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);outline:none;z-index:10052;margin:0}.ui-timepicker-wrapper.ui-timepicker-with-duration{width:13em}.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-30,.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-60{width:11em}.ui-timepicker-list{margin:0;padding:0;list-style:none}.ui-timepicker-duration{margin-left:5px;color:#888}.ui-timepicker-list:hover .ui-timepicker-duration{color:#888}.ui-timepicker-list li{padding:3px 0 3px 5px;cursor:pointer;white-space:nowrap;color:#000;list-style:none;margin:0}.ui-timepicker-list:hover .ui-timepicker-selected{background:#fff;color:#000}.ui-timepicker-list .ui-timepicker-selected:hover,.ui-timepicker-list li:hover,li.ui-timepicker-selected{background:#1980EC;color:#fff}.ui-timepicker-list li:hover .ui-timepicker-duration,li.ui-timepicker-selected .ui-timepicker-duration{color:#ccc}.ui-timepicker-list li.ui-timepicker-disabled,.ui-timepicker-list li.ui-timepicker-disabled:hover,.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled{color:#888;cursor:default}.ui-timepicker-list li.ui-timepicker-disabled:hover,.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled{background:#f2f2f2}
3 |
--------------------------------------------------------------------------------
/motioneye/static/fnt/mavenpro-black-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/fnt/mavenpro-black-webfont.woff
--------------------------------------------------------------------------------
/motioneye/static/fnt/mavenpro-bold-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/fnt/mavenpro-bold-webfont.woff
--------------------------------------------------------------------------------
/motioneye/static/fnt/mavenpro-medium-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/fnt/mavenpro-medium-webfont.woff
--------------------------------------------------------------------------------
/motioneye/static/fnt/mavenpro-regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/fnt/mavenpro-regular-webfont.woff
--------------------------------------------------------------------------------
/motioneye/static/img/IEC5007_On_Symbol.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/IEC5008_Off_Symbol.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/apply-progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/img/apply-progress.gif
--------------------------------------------------------------------------------
/motioneye/static/img/arrows.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-alarm-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-alarm-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-light-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-light-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-lock.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-preset.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-record-start.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-record-stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-snapshot.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-unlock.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-zoom-in.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-action-button-zoom-out.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/camera-progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/img/camera-progress.gif
--------------------------------------------------------------------------------
/motioneye/static/img/camera-top-buttons.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/combo-box-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/main-loading-progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/img/main-loading-progress.gif
--------------------------------------------------------------------------------
/motioneye/static/img/modal-progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/img/modal-progress.gif
--------------------------------------------------------------------------------
/motioneye/static/img/motioneye-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/motioneye-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/no-camera.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/no-preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/slider-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/small-progress.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/motioneye/static/img/small-progress.gif
--------------------------------------------------------------------------------
/motioneye/static/img/top-bar-buttons.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/img/validation-error.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/motioneye/static/js/css-browser-selector.min.js:
--------------------------------------------------------------------------------
1 | /*! https://raw.githubusercontent.com/crucifyer/css-browser-selector/master/css_browser_selector.min.js */
2 | /* jQuery 3: .load(fn) => .on('load', fn) */
3 | function css_browser_selector(u){var ua=u.toLowerCase(),is=function(t){return ua.indexOf(t)>-1},g="gecko",w="webkit",s="safari",c="chrome",o="opera",m="mobile",v=0,r=window.devicePixelRatio?(window.devicePixelRatio+"").replace(".","_"):"1";var res=[!/opera|webtv/.test(ua)&&/msie\s(\d+)/.test(ua)&&(v=RegExp.$1*1)?"ie ie"+v+(v==6||v==7?" ie67 ie678 ie6789":v==8?" ie678 ie6789":v==9?" ie6789 ie9m":v>9?" ie9m":""):/edge\/(\d+)\.(\d+)/.test(ua)&&(v=[RegExp.$1,RegExp.$2])?is("chrome/")?"chrome edge":"ie ie"+v[0]+" ie"+v[0]+"_"+v[1]+" ie9m edge":/trident\/\d+.*?;\s*rv:(\d+)\.(\d+)\)/.test(ua)&&(v=[RegExp.$1,RegExp.$2])?"ie ie"+v[0]+" ie"+v[0]+"_"+v[1]+" ie9m":/firefox\/(\d+)\.(\d+)/.test(ua)&&(re=RegExp)?g+" ff ff"+re.$1+" ff"+re.$1+"_"+re.$2:is("gecko/")?g:is(o)?"old"+o+(/version\/(\d+)/.test(ua)?" "+o+RegExp.$1:/opera(\s|\/)(\d+)/.test(ua)?" "+o+RegExp.$2:""):is("konqueror")?"konqueror":is("blackberry")?m+" blackberry":is(c)||is("crios")?w+" "+c:is("iron")?w+" iron":!is("cpu os")&&is("applewebkit/")?w+" "+s:is("mozilla/")?g:"",is("android")?m+" android":"",is("tablet")?"tablet":"",is("opr/")?o:"",is("yabrowser")?"yandex":"",is("j2me")?m+" j2me":is("ipad; u; cpu os")?m+" chrome android tablet":is("ipad;u;cpu os")?m+" chromedef android tablet":is("iphone")?m+" ios iphone":is("ipod")?m+" ios ipod":is("ipad")?m+" ios ipad tablet":is("mac")?"mac":is("darwin")?"mac":is("webtv")?"webtv":is("win")?"win"+(is("windows nt 6.0")?" vista":""):is("freebsd")?"freebsd":is("x11")||is("linux")?"linux":"",r!="1"?" retina ratio"+r:"","js portrait"].join(" ");if(window.jQuery&&!window.jQuery.browser){window.jQuery.browser=v?{msie:1,version:v}:{}}return res}(function(d,w){var _c=css_browser_selector(navigator.userAgent);var h=d.documentElement;h.className+=" "+_c;var _d=_c.replace(/^\s*|\s*$/g,"").split(/ +/);w.CSSBS=1;for(var i=0;i<_d.length;i++){w["CSSBS_"+_d[i]]=1}var _de=function(v){return d.documentElement[v]||d.body[v]};if(w.jQuery){(function($){var p="portrait",l="landscape";var m="smartnarrow",mw="smartwide",t="tabletnarrow",tw="tabletwide",ac=m+" "+mw+" "+t+" "+tw+" pc";var $h=$(h);var to=0,cw=0;function CSSSelectorUpdateSize(){if(to!=0)return;try{var _cw=_de("clientWidth"),_ch=_de("clientHeight");if(_cw>_ch){$h.removeClass(p).addClass(l)}else{$h.removeClass(l).addClass(p)}if(_cw==cw)return;cw=_cw}catch(e){}to=setTimeout(CSSSelectorUpdateSize_,100)}function CSSSelectorUpdateSize_(){try{$h.removeClass(ac);$h.addClass(cw<=360?m:cw<=640?mw:cw<=768?t:cw<=1024?tw:"pc")}catch(e){}to=0}if(w.CSSBS_ie){setInterval(CSSSelectorUpdateSize,1e3)}else{$(w).on("resize orientationchange",CSSSelectorUpdateSize).trigger("resize")}$(w).on('load', CSSSelectorUpdateSize)})(w.jQuery)}})(document,window);
4 |
--------------------------------------------------------------------------------
/motioneye/static/js/frame.js:
--------------------------------------------------------------------------------
1 |
2 | var refreshDisabled = false;
3 |
4 |
5 | /* camera frame */
6 |
7 | function setupCameraFrame() {
8 | var cameraFrameDiv = $('div.camera-frame')
9 | var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder');
10 | var cameraProgress = cameraFrameDiv.find('div.camera-progress');
11 | var cameraImg = cameraFrameDiv.find('img.camera');
12 | var cameraId = cameraFrameDiv.attr('id').substring(6);
13 | var progressImg = cameraFrameDiv.find('img.camera-progress');
14 | var body = $('body');
15 |
16 | cameraFrameDiv[0].refreshDivider = 0;
17 | cameraFrameDiv[0].streamingFramerate = parseInt(cameraFrameDiv.attr('streaming_framerate')) || 1;
18 | cameraFrameDiv[0].streamingServerResize = cameraFrameDiv.attr('streaming_server_resize') == 'true';
19 | cameraFrameDiv[0].proto = cameraFrameDiv.attr('proto');
20 | cameraFrameDiv[0].url = cameraFrameDiv.attr('url');
21 | progressImg.attr('src', staticPath + 'img/camera-progress.gif');
22 |
23 | cameraProgress.addClass('visible');
24 | cameraPlaceholder.css('opacity', '0');
25 |
26 | /* fade in */
27 | cameraFrameDiv.animate({'opacity': 1}, 100);
28 |
29 | /* error and load handlers */
30 | cameraImg.on('error', function () {
31 | this.error = true;
32 | this.loading_count = 0;
33 |
34 | cameraImg.addClass('error').removeClass('loading');
35 | cameraPlaceholder.css('opacity', 1);
36 | cameraProgress.removeClass('visible');
37 | cameraFrameDiv.removeClass('motion-detected');
38 | });
39 | cameraImg.on('load', function () {
40 | if (refreshDisabled) {
41 | return; /* refresh temporarily disabled for updating */
42 | }
43 |
44 | this.error = false;
45 | this.loading_count = 0;
46 |
47 | cameraImg.removeClass('error').removeClass('loading');
48 | cameraPlaceholder.css('opacity', 0);
49 | cameraProgress.removeClass('visible');
50 |
51 | /* there's no point in looking for a cookie update more often than once every second */
52 | var now = new Date().getTime();
53 | if ((!this.lastCookieTime || now - this.lastCookieTime > 1000) && (cameraFrameDiv[0].proto != 'mjpeg')) {
54 | if (getCookie('motion_detected_' + cameraId) == 'true') {
55 | cameraFrameDiv.addClass('motion-detected');
56 | }
57 | else {
58 | cameraFrameDiv.removeClass('motion-detected');
59 | }
60 |
61 | this.lastCookieTime = now;
62 | }
63 |
64 | if (this.naturalWidth / this.naturalHeight > body.width() / body.height()) {
65 | cameraImg.css('width', '100%');
66 | cameraImg.css('height', 'auto');
67 | }
68 | else {
69 | cameraImg.css('width', 'auto');
70 | cameraImg.css('height', '100%');
71 | }
72 | });
73 |
74 | cameraImg.addClass('loading');
75 | }
76 |
77 | function refreshCameraFrame() {
78 | var $cameraFrame = $('div.camera-frame');
79 | var cameraFrame = $cameraFrame[0];
80 | var img = $cameraFrame.find('img.camera')[0];
81 | var cameraId = cameraFrame.id.substring(6);
82 |
83 | if (cameraFrame.proto == 'mjpeg') {
84 | /* no manual refresh for simple mjpeg cameras */
85 | var url = cameraFrame.url.replace('127.0.0.1', window.location.host.split(':')[0]);
86 | url += (url.indexOf('?') > 0 ? '&' : '?') + '_=' + new Date().getTime();
87 | img.src = url;
88 | return;
89 | }
90 |
91 | var count = 1000 / (refreshInterval * cameraFrame.streamingFramerate);
92 |
93 | if (img.error) {
94 | /* in case of error, decrease the refresh rate to 1 fps */
95 | count = 1000 / refreshInterval;
96 | }
97 |
98 | if (cameraFrame.refreshDivider < count) {
99 | cameraFrame.refreshDivider++;
100 | }
101 | else {
102 | (function () {
103 | if (refreshDisabled) {
104 | /* camera refreshing disabled, retry later */
105 |
106 | return;
107 | }
108 |
109 | if (img.loading_count) {
110 | img.loading_count++; /* increases each time the camera would refresh but is still loading */
111 |
112 | if (img.loading_count > 2 * 1000 / refreshInterval) { /* limits the retry at one every two seconds */
113 | img.loading_count = 0;
114 | }
115 | else {
116 | return; /* wait for the previous frame to finish loading */
117 | }
118 | }
119 |
120 | var timestamp = new Date().getTime();
121 | var path = basePath + 'picture/' + cameraId + '/current/?_=' + timestamp;
122 | if (cameraFrame.serverSideResize) {
123 | path += '&width=' + img.width;
124 | }
125 |
126 | path = addAuthParams('GET', path);
127 | img.src = path;
128 | img.loading_count = 1;
129 |
130 | cameraFrame.refreshDivider = 0;
131 | })();
132 | }
133 |
134 | setTimeout(refreshCameraFrame, refreshInterval);
135 | }
136 |
137 |
138 | /* startup function */
139 |
140 | $(document).ready(function () {
141 | setupCameraFrame();
142 | refreshCameraFrame();
143 | });
144 |
--------------------------------------------------------------------------------
/motioneye/static/js/gettext.min.js:
--------------------------------------------------------------------------------
1 | var i18n=function(){"use strict";
2 | /*! gettext.js - Guillaume Potier - MIT Licensed */return function(t){t=t||{},this&&(this.__version="1.1.1");var r={domain:"messages",locale:"undefined"!=typeof document&&document.documentElement.getAttribute("lang")||"en",plural_func:function(t){return{nplurals:2,plural:1!=t?1:0}},ctxt_delimiter:String.fromCharCode(4)},n=function(t){var r=typeof t;return"function"===r||"object"===r&&!!t},e={},l=t.locale||r.locale,a=t.domain||r.domain,o={},i={},u=t.ctxt_delimiter||r.ctxt_delimiter;t.messages&&(o[a]={},o[a][l]=t.messages),t.plural_forms&&(i[l]=t.plural_forms);var s=function(t){var r=arguments;return t.replace(/%%/g,"%% ").replace(/%(\d+)/g,(function(t,n){return r[n]})).replace(/%% /g,"%")},p=function(t){return-1!==t.indexOf(u)?t.split(u)[1]:t},c=function(t){for(var r=[t],n=t.lastIndexOf("-");n>0;)t=t.slice(0,n),r.push(t),n=t.lastIndexOf("-");return r},f=function(t){var r=(t=t.replace("_","-")).search(/[.@]/);return-1!=r&&(t=t.slice(0,r)),t},d=function(t){if(!new RegExp("^\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;n0-9_()])+").test(t))throw new Error(s('The plural form "%1" is not valid',t));return new Function("n","var plural, nplurals; "+t+" return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) };")},g=function(t,r,n){return n.plural_form?(n.plural_func?a=n.plural_func(r):(e[l]||(e[l]=d(i[l])),a=e[l](r)),(void 0===a.plural||a.plural>a.nplurals||t.length<=a.plural)&&(a.plural=0),s.apply(this,[p(t[a.plural]),r].concat(Array.prototype.slice.call(arguments,3)))):s.apply(this,[p(t[0])].concat(Array.prototype.slice.call(arguments,3)));var a};return{strfmt:s,expand_locale:c,__:function(){return this.gettext.apply(this,arguments)},_n:function(){return this.ngettext.apply(this,arguments)},_p:function(){return this.pgettext.apply(this,arguments)},setMessages:function(t,r,e,l){if(!t||!r||!e)throw new Error("You must provide a domain, a locale and messages");if("string"!=typeof t||"string"!=typeof r||!n(e))throw new Error("Invalid arguments");return r=f(r),l&&(i[r]=l),o[t]||(o[t]={}),o[t][r]=e,this},loadJSON:function(t,e){if(n(t)||(t=JSON.parse(t)),!t[""]||!t[""].language||!t[""]["plural-forms"])throw new Error('Wrong JSON, it must have an empty key ("") with "language" and "plural-forms" information');var l=t[""];return delete t[""],this.setMessages(e||r.domain,l.language,t,l["plural-forms"])},setLocale:function(t){return l=f(t),this},getLocale:function(){return l},textdomain:function(t){return t?(a=t,this):a},gettext:function(t){return this.dcnpgettext.apply(this,[void 0,void 0,t,void 0,void 0].concat(Array.prototype.slice.call(arguments,1)))},ngettext:function(t,r,n){return this.dcnpgettext.apply(this,[void 0,void 0,t,r,n].concat(Array.prototype.slice.call(arguments,3)))},pgettext:function(t,r){return this.dcnpgettext.apply(this,[void 0,t,r,void 0,void 0].concat(Array.prototype.slice.call(arguments,2)))},dcnpgettext:function(t,n,e,i,s){if(t=t||a,"string"!=typeof e)throw new Error(this.strfmt('Msgid "%1" is not a valid translatable string',e));var p,f,d,h={plural_form:!1},m=n?n+u+e:e,y=c(l);for(var v in y)if(d=y[v],f=o[t]&&o[t][d]&&o[t][d][m],f=i?f&&"string"!=typeof o[t][d][m]:f&&"string"==typeof o[t][d][m])break;return f?p=o[t][d][m]:(p=e,h.plural_func=r.plural_func),i?(h.plural_form=!0,g.apply(this,[f?p:[e,i],s,h].concat(Array.prototype.slice.call(arguments,5)))):g.apply(this,[[p],s,h].concat(Array.prototype.slice.call(arguments,5)))}}}}();
3 |
--------------------------------------------------------------------------------
/motioneye/static/js/jquery.mousewheel.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Mousewheel 3.1.13
3 | * Copyright OpenJS Foundation and other contributors
4 | */
5 | !function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(a){var u,r,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],f=Array.prototype.slice;if(a.event.fixHooks)for(var n=e.length;n;)a.event.fixHooks[e[--n]]=a.event.mouseHooks;var d=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],i,!1);else this.onmousewheel=i;a.data(this,"mousewheel-line-height",d.getLineHeight(this)),a.data(this,"mousewheel-page-height",d.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],i,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=a(e),e=t["offsetParent"in a.fn?"offsetParent":"parent"]();return e.length||(e=a("body")),parseInt(e.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return a(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function i(e){var t,n=e||window.event,i=f.call(arguments,1),o=0,l=0,s=0,h=0;if((e=a.event.fix(n)).type="mousewheel","detail"in n&&(s=-1*n.detail),"wheelDelta"in n&&(s=n.wheelDelta),"wheelDeltaY"in n&&(s=n.wheelDeltaY),"wheelDeltaX"in n&&(l=-1*n.wheelDeltaX),"axis"in n&&n.axis===n.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in n&&(o=s=-1*n.deltaY),"deltaX"in n&&(l=n.deltaX,0===s&&(o=-1*l)),0!==s||0!==l)return 1===n.deltaMode?(o*=t=a.data(this,"mousewheel-line-height"),s*=t,l*=t):2===n.deltaMode&&(o*=t=a.data(this,"mousewheel-page-height"),s*=t,l*=t),h=Math.max(Math.abs(s),Math.abs(l)),(!r||h.
16 |
17 | import calendar
18 | import datetime
19 | import logging
20 | import multiprocessing
21 | import os
22 | import pickle
23 | import time
24 |
25 | from tornado.ioloop import IOLoop
26 |
27 | from motioneye import settings
28 |
29 | _INTERVAL = 2
30 | _STATE_FILE_NAME = 'tasks.pickle'
31 | _MAX_TASKS = 100
32 |
33 | # we must be sure there's only one extra process that handles all tasks
34 | # TODO replace the pool with one simple thread
35 | _POOL_SIZE = 1
36 |
37 | _tasks = []
38 | _pool = None
39 |
40 |
41 | def start():
42 | global _pool
43 |
44 | io_loop = IOLoop.current()
45 | io_loop.add_timeout(datetime.timedelta(seconds=_INTERVAL), _check_tasks)
46 |
47 | def init_pool_process():
48 | import signal
49 |
50 | signal.signal(signal.SIGINT, signal.SIG_IGN)
51 | signal.signal(signal.SIGTERM, signal.SIG_IGN)
52 |
53 | _load()
54 | _pool = multiprocessing.Pool(_POOL_SIZE, initializer=init_pool_process)
55 |
56 |
57 | def stop():
58 | global _pool
59 |
60 | _pool = None
61 |
62 |
63 | def add(when, func, tag=None, callback=None, **params):
64 | if len(_tasks) >= _MAX_TASKS:
65 | return logging.error(
66 | 'the maximum number of tasks (%d) has been reached' % _MAX_TASKS
67 | )
68 |
69 | now = time.time()
70 |
71 | if isinstance(when, int): # delay, in seconds
72 | when += now
73 |
74 | elif isinstance(when, datetime.timedelta):
75 | when = now + when.total_seconds()
76 |
77 | elif isinstance(when, datetime.datetime):
78 | when = calendar.timegm(when.timetuple())
79 |
80 | i = 0
81 | while i < len(_tasks) and _tasks[i][0] <= when:
82 | i += 1
83 |
84 | logging.debug('adding task "%s" in %d seconds' % (tag or func.__name__, when - now))
85 | _tasks.insert(i, (when, func, tag, callback, params))
86 |
87 | _save()
88 |
89 |
90 | def _check_tasks():
91 | io_loop = IOLoop.current()
92 | io_loop.add_timeout(datetime.timedelta(seconds=_INTERVAL), _check_tasks)
93 |
94 | now = time.time()
95 | changed = False
96 | while _tasks and _tasks[0][0] <= now:
97 | (when, func, tag, callback, params) = _tasks.pop(0) # @UnusedVariable
98 |
99 | logging.debug('executing task "%s"' % tag or func.__name__)
100 | _pool.apply_async(
101 | func, kwds=params, callback=callback if callable(callback) else None
102 | )
103 |
104 | changed = True
105 |
106 | if changed:
107 | _save()
108 |
109 |
110 | def _load():
111 | global _tasks
112 |
113 | _tasks = []
114 |
115 | file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME)
116 |
117 | if os.path.exists(file_path):
118 | logging.debug('loading tasks from "%s"...' % file_path)
119 |
120 | try:
121 | f = open(file_path, 'rb')
122 |
123 | except Exception as e:
124 | logging.error(f'could not open tasks file "{file_path}": {e}')
125 |
126 | return
127 |
128 | try:
129 | _tasks = pickle.load(f)
130 |
131 | except Exception as e:
132 | logging.error(f'could not read tasks from file "{file_path}": {e}')
133 |
134 | finally:
135 | f.close()
136 |
137 |
138 | def _save():
139 | file_path = os.path.join(settings.CONF_PATH, _STATE_FILE_NAME)
140 |
141 | logging.debug('saving tasks to "%s"...' % file_path)
142 |
143 | try:
144 | f = open(file_path, 'wb')
145 |
146 | except Exception as e:
147 | logging.error(f'could not open tasks file "{file_path}": {e}')
148 |
149 | return
150 |
151 | try:
152 | # don't save tasks that have a callback
153 | tasks = [t for t in _tasks if not t[3]]
154 | pickle.dump(tasks, f)
155 |
156 | except Exception as e:
157 | logging.error(f'could not save tasks to file "{file_path}": {e}')
158 |
159 | finally:
160 | f.close()
161 |
--------------------------------------------------------------------------------
/motioneye/template.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import gettext
18 |
19 | from babel.support import Translations
20 | from jinja2 import Environment, FileSystemLoader, select_autoescape
21 |
22 | from motioneye import settings
23 | from motioneye.utils.dtconv import (
24 | pretty_date,
25 | pretty_date_time,
26 | pretty_duration,
27 | pretty_time,
28 | )
29 |
30 | _jinja_env = None
31 |
32 |
33 | def _init_jinja():
34 | global _jinja_env
35 |
36 | # loader=FileSystemLoader(searchpath="templates"),
37 | _jinja_env = Environment(
38 | loader=FileSystemLoader(settings.TEMPLATE_PATH),
39 | trim_blocks=False,
40 | extensions=['jinja2.ext.i18n'],
41 | autoescape=select_autoescape(['html', 'xml']),
42 | )
43 | _jinja_env.install_gettext_translations(settings.traduction, newstyle=True)
44 |
45 | # globals
46 | _jinja_env.globals['settings'] = settings
47 |
48 | # filters
49 | _jinja_env.filters['pretty_date_time'] = pretty_date_time
50 | _jinja_env.filters['pretty_date'] = pretty_date
51 | _jinja_env.filters['pretty_time'] = pretty_time
52 | _jinja_env.filters['pretty_duration'] = pretty_duration
53 |
54 |
55 | def _reload_lang():
56 | _jinja_env.install_gettext_translations(settings.traduction, newstyle=True)
57 | _jinja_env.globals['settings'] = settings
58 |
59 |
60 | def add_template_path(path):
61 | if _jinja_env is None:
62 | _init_jinja()
63 |
64 | _jinja_env.loader.searchpath.append(path)
65 |
66 |
67 | def add_context(name, value):
68 | if _jinja_env is None:
69 | _init_jinja()
70 |
71 | _jinja_env.globals[name] = value
72 |
73 |
74 | def render(template_name, **context):
75 | if _jinja_env is None:
76 | _init_jinja()
77 |
78 | template = _jinja_env.get_template(template_name)
79 | return template.render(**context)
80 |
--------------------------------------------------------------------------------
/motioneye/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block meta %}
5 |
6 |
7 |
8 |
9 |
10 |
11 | {% endblock %}
12 | {% block title %}{% endblock %}
13 | {% block style %}
14 |
15 |
16 |
17 |
18 | {% endblock %}
19 | {% block script %}
20 |
21 |
22 |
23 |
24 | {% endblock %}
25 |
26 |
27 | {% block body %}{% endblock %}
28 | {% block inline %}{% endblock %}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/motioneye/templates/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "lang": "en-US",
3 | "short_name": "motionEye",
4 | "name": "motionEye",
5 | "description": "A free video surveillance software.",
6 | "version": "{{version}}",
7 | "theme_color": "#414141",
8 | "background_color": "#414141",
9 | "icons": [
10 | {"src": "{{static_path}}img/motioneye-icon.svg?v={{version}}", "sizes": "any"}
11 | ],
12 | "start_url": ".",
13 | "display": "standalone"
14 | }
15 |
--------------------------------------------------------------------------------
/motioneye/templates/version.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block script %}
4 | {{super()}}
5 |
9 |
10 | {% endblock %}
11 |
12 | {% block body %}
13 | hostname = "{{hostname}}"
14 | version = "{{version}}"
15 | motion_version = "{{motion_version}}"
16 | os_version = "{{os_version}}"
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/motioneye/update.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import datetime
18 | import logging
19 | import re
20 |
21 | from tornado import ioloop
22 |
23 | from motioneye import utils
24 |
25 |
26 | def get_os_version():
27 | try:
28 | import platformupdate
29 |
30 | return platformupdate.get_os_version()
31 |
32 | except ImportError:
33 | return _get_os_version_lsb_release()
34 |
35 |
36 | def _get_os_version_lsb_release():
37 | try:
38 | output = utils.call_subprocess('lsb_release -sri', shell=True)
39 | lines = output.strip().split()
40 | name, version = lines
41 | if version.lower() == 'rolling':
42 | version = ''
43 |
44 | return name, version
45 |
46 | except:
47 | return _get_os_version_uname()
48 |
49 |
50 | def _get_os_version_uname():
51 | try:
52 | output = utils.call_subprocess('uname -rs', shell=True)
53 | lines = output.strip().split()
54 | name, version = lines
55 |
56 | return name, version
57 |
58 | except:
59 | return 'Linux', '' # most likely :)
60 |
61 |
62 | def compare_versions(version1, version2):
63 | version1 = re.sub('[^0-9.]', '', version1)
64 | version2 = re.sub('[^0-9.]', '', version2)
65 |
66 | def int_or_0(n):
67 | try:
68 | return int(n)
69 |
70 | except:
71 | return 0
72 |
73 | version1 = [int_or_0(n) for n in version1.split('.')]
74 | version2 = [int_or_0(n) for n in version2.split('.')]
75 |
76 | len1 = len(version1)
77 | len2 = len(version2)
78 | length = min(len1, len2)
79 | for i in range(length):
80 | p1 = version1[i]
81 | p2 = version2[i]
82 |
83 | if p1 < p2:
84 | return -1
85 |
86 | elif p1 > p2:
87 | return 1
88 |
89 | if len1 < len2:
90 | return -1
91 |
92 | elif len1 > len2:
93 | return 1
94 |
95 | else:
96 | return 0
97 |
98 |
99 | def get_all_versions():
100 | try:
101 | import platformupdate
102 |
103 | except ImportError:
104 | return []
105 |
106 | return platformupdate.get_all_versions()
107 |
108 |
109 | def perform_update(version):
110 | logging.info(f'updating to version {version}...')
111 |
112 | try:
113 | import platformupdate
114 |
115 | except ImportError:
116 | logging.error('updating is not available on this platform')
117 |
118 | raise Exception('updating is not available on this platform')
119 |
120 | # schedule the actual update for two seconds later,
121 | # since we want to be able to respond to the request right away
122 | ioloop.IOLoop.current().add_timeout(
123 | datetime.timedelta(seconds=2), platformupdate.perform_update, version=version
124 | )
125 |
--------------------------------------------------------------------------------
/motioneye/utils/dtconv.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import datetime
19 | from typing import Union
20 |
21 | __all__ = ('pretty_date_time', 'pretty_date', 'pretty_duration', 'pretty_time')
22 |
23 |
24 | def pretty_date_time(date_time, tzinfo=None, short=False):
25 | if date_time is None:
26 | return '(' + _('neniam') + ')'
27 |
28 | if isinstance(date_time, int):
29 | return pretty_date_time(datetime.datetime.fromtimestamp(date_time))
30 |
31 | if short:
32 | text = '{day} {month}, {hm}'.format(
33 | day=date_time.day,
34 | month=date_time.strftime('%b'),
35 | hm=date_time.strftime('%H:%M'),
36 | )
37 |
38 | else:
39 | text = '{day} {month} {year}, {hm}'.format(
40 | day=date_time.day,
41 | month=date_time.strftime('%B'),
42 | year=date_time.year,
43 | hm=date_time.strftime('%H:%M'),
44 | )
45 |
46 | if tzinfo:
47 | offset = tzinfo.utcoffset(datetime.datetime.utcnow()).seconds
48 | tz = 'GMT'
49 | if offset >= 0:
50 | tz += '+'
51 |
52 | else:
53 | tz += '-'
54 | offset = -offset
55 |
56 | tz += '%.2d' % (offset // 3600) + ':%.2d' % ((offset % 3600) // 60)
57 |
58 | text += ' (' + tz + ')'
59 |
60 | return text
61 |
62 |
63 | def pretty_date(d: Union[datetime.date, int]) -> str:
64 | if d is None:
65 | return '(' + _('neniam') + ')'
66 |
67 | if isinstance(d, int):
68 | return pretty_date(datetime.datetime.fromtimestamp(d))
69 |
70 | return '{day} {month} {year}'.format(
71 | day=d.day, month=_(d.strftime('%B')), year=d.year
72 | )
73 |
74 |
75 | def pretty_time(t: Union[datetime.time, datetime.timedelta]) -> str:
76 | if t is None:
77 | return ''
78 |
79 | if isinstance(t, datetime.timedelta):
80 | hour = t.seconds // 3600
81 | minute = (t.seconds % 3600) // 60
82 | t = datetime.time(hour=hour, minute=minute)
83 |
84 | return '{hm}'.format(hm=t.strftime('%H:%M'))
85 |
86 |
87 | def pretty_duration(duration):
88 | if duration is None:
89 | duration = 0
90 |
91 | if isinstance(duration, datetime.timedelta):
92 | duration = duration.seconds + duration.days * 86400
93 |
94 | if duration < 0:
95 | negative = True
96 | duration = -duration
97 |
98 | else:
99 | negative = False
100 |
101 | days = duration // 86400
102 | duration %= 86400
103 | hours = duration // 3600
104 | duration %= 3600
105 | minutes = duration // 60
106 | duration %= 60
107 | seconds = duration
108 |
109 | # treat special cases
110 | special_result = None
111 | if days != 0 and hours == 0 and minutes == 0 and seconds == 0:
112 | if days == 1:
113 | special_result = str(days) + ' ' + _('tago')
114 |
115 | elif days == 7:
116 | special_result = '1 ' + _('semajno')
117 |
118 | elif days in [30, 31, 32]:
119 | special_result = '1 ' + _('monato')
120 |
121 | elif days in [365, 366]:
122 | special_result = '1 ' + _('jaro')
123 |
124 | else:
125 | special_result = str(days) + ' ' + _('tagoj')
126 |
127 | elif days == 0 and hours != 0 and minutes == 0 and seconds == 0:
128 | if hours == 1:
129 | special_result = str(hours) + ' ' + _('horo')
130 |
131 | else:
132 | special_result = str(hours) + ' ' + _('horoj')
133 |
134 | elif days == 0 and hours == 0 and minutes != 0 and seconds == 0:
135 | if minutes == 1:
136 | special_result = str(minutes) + ' ' + _('minuto')
137 |
138 | else:
139 | special_result = str(minutes) + ' ' + _('minutoj')
140 |
141 | elif days == 0 and hours == 0 and minutes == 0 and seconds != 0:
142 | if seconds == 1:
143 | special_result = str(seconds) + ' ' + _('sekundo')
144 |
145 | else:
146 | special_result = str(seconds) + ' ' + _('sekundoj')
147 |
148 | elif days == 0 and hours == 0 and minutes == 0 and seconds == 0:
149 | special_result = str(0) + ' ' + _('sekundoj')
150 |
151 | if special_result:
152 | if negative:
153 | special_result = _('minus') + ' ' + special_result
154 |
155 | return special_result
156 |
157 | if days:
158 | fmt = "{d}d{h}h{m}m"
159 |
160 | elif hours:
161 | fmt = "{h}h{m}m"
162 |
163 | elif minutes:
164 | fmt = "{m}m"
165 | if seconds:
166 | fmt += "{s}s"
167 |
168 | else:
169 | fmt = "{s}s"
170 |
171 | if negative:
172 | fmt = '-' + fmt
173 |
174 | return fmt.format(d=days, h=hours, m=minutes, s=seconds)
175 |
--------------------------------------------------------------------------------
/motioneye/utils/http.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from dataclasses import dataclass
3 | from typing import Any, Hashable, Union
4 |
5 | __all__ = ('RtmpUrl', 'RtspUrl', 'MjpegUrl')
6 |
7 |
8 | @dataclass
9 | class StreamUrl:
10 | scheme: str
11 | port: str
12 | host: str = '127.0.0.1'
13 | path: str = ''
14 | username: Union[None, str] = None
15 | password: Union[None, str] = None
16 |
17 | _tpl = '%(scheme)s://%(host)s%(port)s%(path)s'
18 |
19 | def __str__(self):
20 | return self._tpl % dict(
21 | scheme=self.scheme,
22 | host=self.host,
23 | port=(':' + str(self.port)) if self.port else '',
24 | path=self.path,
25 | username=self.username,
26 | password=self.password,
27 | )
28 |
29 | @classmethod
30 | def _get_dict_field_val(cls, k: Union[str, Hashable], v: Any) -> Any:
31 | try:
32 | return v or getattr(cls, k)
33 | except AttributeError:
34 | return v
35 |
36 | @classmethod
37 | def from_dict(cls, d: dict): # -> typing.Self in Python >= v3.11
38 | return cls(
39 | **{
40 | k: cls._get_dict_field_val(k, v)
41 | for k, v in d.items()
42 | if k in inspect.signature(cls).parameters
43 | }
44 | )
45 |
46 |
47 | @dataclass
48 | class RtmpUrl(StreamUrl):
49 | scheme: str = 'rtmp'
50 | port: str = '1935'
51 |
52 |
53 | @dataclass
54 | class RtspUrl(StreamUrl):
55 | scheme: str = 'rtsp'
56 | port: str = '554'
57 |
58 |
59 | @dataclass
60 | class MjpegUrl(StreamUrl):
61 | scheme: str = 'http'
62 | port: str = '80'
63 |
--------------------------------------------------------------------------------
/motioneye/utils/mjpeg.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import logging
19 | import re
20 | from typing import List
21 |
22 | from tornado.concurrent import Future
23 | from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse
24 |
25 | from motioneye import settings
26 | from motioneye.utils import GetCamerasResponse, cast_future, pretty_http_error
27 | from motioneye.utils.http import MjpegUrl
28 |
29 | __all__ = ('test_mjpeg_url',)
30 |
31 |
32 | def test_mjpeg_url(
33 | data: dict, auth_modes: List[str], allow_jpeg: bool
34 | ) -> 'Future[GetCamerasResponse]':
35 | url_obj = MjpegUrl.from_dict(data)
36 | url = str(url_obj)
37 |
38 | called = [False]
39 | status_2xx = [False]
40 | http_11 = [False]
41 |
42 | future = Future()
43 |
44 | def do_request() -> 'Future[HTTPResponse]':
45 | if url_obj.username:
46 | auth = auth_modes[0]
47 |
48 | else:
49 | auth = 'no'
50 |
51 | logging.debug(f'testing (m)jpg netcam at {url} using {auth} authentication')
52 |
53 | request = HTTPRequest(
54 | url,
55 | auth_username=url_obj.username,
56 | auth_password=url_obj.password or '',
57 | auth_mode=auth_modes.pop(0),
58 | connect_timeout=settings.REMOTE_REQUEST_TIMEOUT,
59 | request_timeout=settings.REMOTE_REQUEST_TIMEOUT,
60 | header_callback=on_header,
61 | validate_cert=settings.VALIDATE_CERTS,
62 | )
63 |
64 | fetch_future = cast_future(
65 | AsyncHTTPClient(force_instance=True).fetch(request, raise_error=False)
66 | )
67 | fetch_future.add_done_callback(on_response)
68 | return fetch_future
69 |
70 | def on_header(header: str):
71 | header = header.lower()
72 | if header.startswith('content-type') and status_2xx[0]:
73 | content_type = header.split(':')[1].strip()
74 | called[0] = True
75 |
76 | if content_type in ['image/jpg', 'image/jpeg', 'image/pjpg'] and allow_jpeg:
77 | future.set_result(
78 | GetCamerasResponse(
79 | [
80 | {
81 | 'id': 1,
82 | 'name': 'JPEG Network Camera',
83 | 'keep_alive': http_11[0],
84 | }
85 | ],
86 | None,
87 | )
88 | )
89 |
90 | elif content_type.startswith('multipart/x-mixed-replace'):
91 | future.set_result(
92 | GetCamerasResponse(
93 | [
94 | {
95 | 'id': 1,
96 | 'name': 'MJPEG Network Camera',
97 | 'keep_alive': http_11[0],
98 | }
99 | ],
100 | None,
101 | )
102 | )
103 |
104 | else:
105 | future.set_result(
106 | GetCamerasResponse(None, error='not a supported network camera')
107 | )
108 |
109 | else:
110 | # check for the status header
111 | m = re.match(r'^http/1.(\d) (\d+) ', header)
112 | if m:
113 | if int(m.group(2)) / 100 == 2:
114 | status_2xx[0] = True
115 |
116 | if m.group(1) == '1':
117 | http_11[0] = True
118 |
119 | def on_response(res_future: Future):
120 | response = res_future.result()
121 |
122 | if not called[0]:
123 | if response.code == 401 and auth_modes and url_obj.username:
124 | status_2xx[0] = False
125 | do_request()
126 |
127 | else:
128 | called[0] = True
129 | error = (
130 | pretty_http_error(response)
131 | if response.error
132 | else 'not a supported network camera'
133 | )
134 | future.set_result(GetCamerasResponse(None, error=error))
135 |
136 | do_request()
137 |
138 | return future
139 |
--------------------------------------------------------------------------------
/motioneye/utils/rtmp.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | from motioneye.utils import GetCamerasResponse
19 | from motioneye.utils.http import RtmpUrl
20 |
21 | __all__ = ('test_rtmp_url',)
22 |
23 |
24 | def test_rtmp_url(data: dict) -> GetCamerasResponse:
25 | url_obj = RtmpUrl.from_dict(data)
26 |
27 | # Since RTMP is a binary TCP stream its a little more work to do a proper test
28 | # For now lets just check if a TCP socket is open on the target IP:PORT
29 | # TODO: Actually do the TCP SYN/ACK check...
30 |
31 | cameras = [{'id': 'tcp', 'name': 'RTMP/TCP Camera'}]
32 | return GetCamerasResponse(cameras, None)
33 |
--------------------------------------------------------------------------------
/motioneye/webhook.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013 Calin Crisan
2 | # This file is part of motionEye.
3 | #
4 | # motionEye is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import json
18 | import logging
19 | import urllib.error
20 | import urllib.parse
21 | import urllib.request
22 |
23 | from motioneye import settings
24 |
25 |
26 | def parse_options(parser, args):
27 | parser.add_argument('method', help='the HTTP method to use')
28 | parser.add_argument('url', help='the URL for the request')
29 |
30 | return parser.parse_args(args)
31 |
32 |
33 | def main(parser, args):
34 | from motioneye import meyectl, utils
35 |
36 | options = parse_options(parser, args)
37 |
38 | meyectl.configure_logging('webhook', options.log_to_file)
39 | meyectl.configure_tornado()
40 |
41 | logging.debug('hello!')
42 | logging.debug('method = %s' % options.method)
43 | logging.debug('url = %s' % options.url)
44 |
45 | headers = {}
46 | parts = urllib.parse.urlparse(options.url)
47 | url = options.url
48 | data = None
49 |
50 | if options.method == 'POST':
51 | headers['Content-Type'] = 'text/plain'
52 | data = b''
53 |
54 | elif options.method == 'POSTf': # form url-encoded
55 | headers['Content-Type'] = 'application/x-www-form-urlencoded'
56 | data = parts.query.encode()
57 | url = options.url.split('?')[0]
58 |
59 | elif options.method == 'POSTj': # json
60 | headers['Content-Type'] = 'application/json'
61 | data = urllib.parse.parse_qs(parts.query)
62 | data = {k: v[0] for (k, v) in list(data.items())}
63 | data = json.dumps(data).encode()
64 | url = options.url.split('?')[0]
65 |
66 | else: # GET
67 | pass
68 |
69 | request = urllib.request.Request(url, data, headers=headers)
70 | try:
71 | utils.urlopen(request, timeout=settings.REMOTE_REQUEST_TIMEOUT)
72 | logging.debug('webhook successfully called')
73 |
74 | except Exception as e:
75 | logging.error('failed to call webhook: %s' % e)
76 |
77 | logging.debug('bye!')
78 |
--------------------------------------------------------------------------------
/motioneye/wsswitch.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2020 Vlsarro
2 | # Copyright (c) 2013 Calin Crisan
3 | # This file is part of motionEye.
4 | #
5 | # motionEye is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import datetime
19 | import logging
20 |
21 | from tornado.ioloop import IOLoop
22 |
23 | from motioneye import config, motionctl, utils
24 |
25 |
26 | def _start_check_ws() -> None:
27 | IOLoop.current().spawn_callback(_check_ws)
28 |
29 |
30 | def start() -> None:
31 | io_loop = IOLoop.current()
32 | io_loop.add_timeout(datetime.timedelta(seconds=1), _start_check_ws)
33 |
34 |
35 | def _during_working_schedule(now, working_schedule) -> bool:
36 | parts = working_schedule.split('|')
37 | if len(parts) < 7:
38 | return False # invalid ws
39 |
40 | ws_day = parts[now.weekday()]
41 | parts = ws_day.split('-')
42 | if len(parts) != 2:
43 | return False # invalid ws
44 |
45 | _from, to = parts
46 | if not _from or not to:
47 | return False # ws disabled for this day
48 |
49 | _from = _from.split(':')
50 | to = to.split(':')
51 | if len(_from) != 2 or len(to) != 2:
52 | return False # invalid ws
53 |
54 | try:
55 | from_h = int(_from[0])
56 | from_m = int(_from[1])
57 | to_h = int(to[0])
58 | to_m = int(to[1])
59 |
60 | except ValueError:
61 | return False # invalid ws
62 |
63 | if now.hour < from_h or now.hour > to_h:
64 | return False
65 |
66 | if now.hour == from_h and now.minute < from_m:
67 | return False
68 |
69 | if now.hour == to_h and now.minute > to_m:
70 | return False
71 |
72 | return True
73 |
74 |
75 | async def _switch_motion_detection_status(
76 | camera_id,
77 | must_be_enabled,
78 | working_schedule_type,
79 | motion_detection_resp: utils.GetMotionDetectionResult,
80 | ) -> None:
81 | if motion_detection_resp.error: # could not detect current status
82 | return logging.warning(
83 | 'skipping motion detection status update for camera with id {id}: {error}'.format(
84 | id=camera_id, error=motion_detection_resp.error
85 | )
86 | )
87 |
88 | if motion_detection_resp.enabled and not must_be_enabled:
89 | logging.debug(
90 | 'must disable motion detection for camera with id {id} ({what} working schedule)'.format(
91 | id=camera_id, what=working_schedule_type
92 | )
93 | )
94 |
95 | await motionctl.set_motion_detection(camera_id, False)
96 |
97 | elif not motion_detection_resp.enabled and must_be_enabled:
98 | logging.debug(
99 | 'must enable motion detection for camera with id {id} ({what} working schedule)'.format(
100 | id=camera_id, what=working_schedule_type
101 | )
102 | )
103 |
104 | await motionctl.set_motion_detection(camera_id, True)
105 |
106 |
107 | async def _check_ws() -> None:
108 | # schedule the next call
109 | io_loop = IOLoop.current()
110 | io_loop.add_timeout(datetime.timedelta(seconds=10), _start_check_ws)
111 |
112 | if not motionctl.running():
113 | return
114 |
115 | now = datetime.datetime.now()
116 | for camera_id in config.get_camera_ids():
117 | camera_config = config.get_camera(camera_id)
118 | if not utils.is_local_motion_camera(camera_config):
119 | continue
120 |
121 | working_schedule = camera_config.get('@working_schedule')
122 | motion_detection = camera_config.get('@motion_detection')
123 | working_schedule_type = camera_config.get('@working_schedule_type') or 'outside'
124 |
125 | if (
126 | not working_schedule
127 | ): # working schedule disabled, motion detection left untouched
128 | continue
129 |
130 | if not motion_detection: # motion detection explicitly disabled
131 | continue
132 |
133 | now_during = _during_working_schedule(now, working_schedule)
134 | must_be_enabled = (now_during and working_schedule_type == 'during') or (
135 | not now_during and working_schedule_type == 'outside'
136 | )
137 |
138 | motion_detection_resp = await motionctl.get_motion_detection(camera_id)
139 | await _switch_motion_detection_status(
140 | camera_id, must_be_enabled, working_schedule_type, motion_detection_resp
141 | )
142 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "setuptools>=42",
4 | "wheel"
5 | ]
6 | build-backend = "setuptools.build_meta"
7 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = motioneye
3 | version = attr: motioneye.VERSION
4 | author = Calin Crisan, Jean Michault, ...
5 | author_email = author@example.com
6 | description = motioneye, a multilingual web interface for motion.
7 | url = https://github.com/motioneye-project/motioneye
8 | keywords = motion, video, surveillance, frontend
9 | project_urls =
10 | Bug Tracker = https://github.com/motioneye-project/motioneye/issues
11 | classifiers =
12 | Programming Language :: Python :: 3
13 | License :: OSI Approved :: GNU General Public License v3 (GPLv3)
14 | Operating System :: POSIX :: Linux
15 | license = GNU General Public License v3.0
16 | license_files = LICENSE
17 | long_description = file: README.md
18 | long_description_content_type = text/markdown
19 |
20 | [options]
21 | packages = motioneye
22 | include_package_data = False
23 | python_requires = >=3.7
24 | install_requires =
25 | tornado
26 | jinja2
27 | pillow
28 | pycurl
29 | babel
30 | boto3
31 |
32 | [options.package_data]
33 | motioneye = extra/*, static/*.*, static/*/*, templates/*, scripts/*, controls/*, handlers/*, utils/*, locale/*/LC_MESSAGES/*.mo
34 |
35 | [options.entry_points]
36 | console_scripts =
37 | meyectl=motioneye.meyectl:main
38 | motioneye_init=motioneye.motioneye_init:main
39 |
40 | [codespell]
41 | ignore-words-list = ot
42 | skip = *.html,*.js,*.json,*.po,*.pot
43 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | from tornado.testing import AsyncHTTPTestCase
4 | from tornado.web import Application
5 |
6 | __all__ = ('AsyncMock', 'WebTestCase')
7 |
8 |
9 | class AsyncMock(mock.MagicMock):
10 | def __call__(self, *args, **kwargs):
11 | sup = super()
12 |
13 | async def coro():
14 | return sup.__call__(*args, **kwargs)
15 |
16 | return coro()
17 |
18 | def __await__(self):
19 | return self().__await__()
20 |
21 |
22 | class WebTestCase(AsyncHTTPTestCase):
23 | handler: type = None
24 |
25 | def get_app(self):
26 | self.app = Application(self.get_handlers(), **self.get_app_kwargs())
27 | return self.app
28 |
29 | def get_handlers(self):
30 | return [('/', self.handler)]
31 |
32 | def get_app_kwargs(self):
33 | return {}
34 |
--------------------------------------------------------------------------------
/tests/test_handlers/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Type, TypeVar
2 | from unittest.mock import MagicMock
3 |
4 | from tornado.testing import AsyncHTTPTestCase
5 | from tornado.web import Application, RequestHandler
6 |
7 | from motioneye.server import make_app
8 |
9 | __all__ = ('HandlerTestCase',)
10 |
11 |
12 | T = TypeVar('T', bound=RequestHandler)
13 |
14 |
15 | class HandlerTestCase(AsyncHTTPTestCase):
16 | handler_cls = NotImplemented # type: Type[T]
17 |
18 | def get_app(self) -> Application:
19 | self.app = make_app()
20 | return self.app
21 |
22 | def get_handler(self, request: MagicMock = None) -> T:
23 | req = request or MagicMock()
24 | return self.handler_cls(self.app, req)
25 |
--------------------------------------------------------------------------------
/tests/test_handlers/test_base.py:
--------------------------------------------------------------------------------
1 | import json
2 | from unittest.mock import MagicMock
3 |
4 | import tornado.testing
5 |
6 | from motioneye.handlers.base import BaseHandler
7 | from tests.test_handlers import HandlerTestCase
8 |
9 |
10 | class BaseHandlerTest(HandlerTestCase):
11 | handler_cls = BaseHandler
12 |
13 | def test_get_argument(self):
14 | handler = self.get_handler(MagicMock(body='{"myarg":5}'))
15 | result = handler.get_argument('myarg')
16 | self.assertEqual(5, result)
17 |
18 | def test_get_argument_no_object_in_json(self):
19 | handler = self.get_handler(MagicMock(body='"{{{"'))
20 | with self.assertRaises(AttributeError):
21 | handler.get_argument('myarg')
22 |
23 | def test_get_argument_empty_json(self):
24 | handler = self.get_handler(MagicMock(body='""'))
25 | result = handler.get_argument('myarg')
26 | self.assertIsNone(result)
27 |
28 | def test_get_argument_invalid_json(self):
29 | handler = self.get_handler(MagicMock(body='{{{{'))
30 | with self.assertRaises(json.decoder.JSONDecodeError):
31 | handler.get_argument('myarg')
32 |
33 | def test_get_current_user(self):
34 | handler = self.get_handler(MagicMock(body='""'))
35 | result = handler.get_current_user()
36 | self.assertEqual('normal', result)
37 |
38 |
39 | if __name__ == '__main__':
40 | tornado.testing.main()
41 |
--------------------------------------------------------------------------------
/tests/test_handlers/test_login.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import tornado.testing
4 |
5 | from motioneye.handlers.login import LoginHandler
6 | from tests.test_handlers import HandlerTestCase
7 |
8 |
9 | class LoginHandlerTest(HandlerTestCase):
10 | handler_cls = LoginHandler
11 |
12 | def test_get_login_no_params(self):
13 | response = self.fetch('/login')
14 | self.assertEqual(200, response.code)
15 | self.assertEqual({}, json.loads(response.body))
16 |
17 | def test_get_login_success(self):
18 | url = '/login/?_=1587216975186&_username=admin&_login=true&_signature=f430e0da555b7714792e9cf9553c22536d00cfc0'
19 | response = self.fetch(url)
20 | self.assertEqual(200, response.code)
21 | self.assertEqual({}, json.loads(response.body))
22 |
23 | def test_get_login_fail(self):
24 | response = self.fetch('/login?_admin=true')
25 | self.assertEqual(403, response.code)
26 | self.assertEqual('application/json', response.headers.get('Content-Type'))
27 | self.assertEqual(
28 | {'error': 'unauthorized', 'prompt': True}, json.loads(response.body)
29 | )
30 |
31 | def test_post(self):
32 | response = self.fetch('/login', method='POST', body='')
33 | self.assertEqual(0, len(response.body))
34 | self.assertEqual('text/html', response.headers.get('Content-Type'))
35 |
36 |
37 | if __name__ == '__main__':
38 | tornado.testing.main()
39 |
--------------------------------------------------------------------------------
/tests/test_utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/motioneye-project/motioneye/b3ed73298554a1db1ea158c4bf6f2ec3a54ef5b9/tests/test_utils/__init__.py
--------------------------------------------------------------------------------
/tests/test_utils/test_http.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from motioneye.utils.http import RtspUrl
4 |
5 |
6 | class TestRTSP(unittest.TestCase):
7 | def test_url_construction(self):
8 | host = '102.170.91.135'
9 | scheme = 'rtsp'
10 | user = 'user1324'
11 | password = 'MyPassword1'
12 | test_data = {
13 | '_': '1589083749971',
14 | 'scheme': scheme,
15 | 'host': host,
16 | 'port': '',
17 | 'path': '/',
18 | 'username': user,
19 | 'password': password,
20 | 'proto': 'netcam',
21 | '_username': 'admin',
22 | '_signature': 'e06ef15af4e73086df6bfa90da0312641a5a2b10',
23 | }
24 | url_obj = RtspUrl.from_dict(test_data)
25 | self.assertEqual(host, url_obj.host)
26 | self.assertEqual(scheme, url_obj.scheme)
27 | self.assertEqual(user, url_obj.username)
28 | self.assertEqual(password, url_obj.password)
29 | self.assertEqual('554', url_obj.port)
30 |
31 |
32 | if __name__ == '__main__':
33 | unittest.main()
34 |
--------------------------------------------------------------------------------
/tests/test_utils/test_mjpeg.py:
--------------------------------------------------------------------------------
1 | import tornado.testing
2 | from tornado.concurrent import Future
3 | from tornado.web import RequestHandler
4 |
5 | from motioneye.utils.mjpeg import test_mjpeg_url
6 | from tests import WebTestCase
7 |
8 |
9 | class UtilsMjpegTest(WebTestCase):
10 | def setUp(self) -> None:
11 | super().setUp()
12 | self.data = None
13 |
14 | def get_handlers(self):
15 | test = self
16 |
17 | class MjpegHandler(RequestHandler):
18 | async def get(self):
19 | if 'image/jpeg' in test.data:
20 | self.set_header('Content-Type', 'image/jpeg')
21 |
22 | if 'mjpeg' in test.data:
23 | self.set_header('Content-Type', 'multipart/x-mixed-replace')
24 | self.write(test.data)
25 | await self.flush()
26 |
27 | return [('/', MjpegHandler)]
28 |
29 | def test_test_mjpeg_url_invalid_data(self):
30 | self.data = 'Some random string'
31 |
32 | callback_result = []
33 |
34 | def mock_on_response(future: Future) -> None:
35 | resp = future.result()
36 | self.stop()
37 | callback_result.append((resp.cameras, resp.error))
38 |
39 | future = test_mjpeg_url(
40 | {'port': self.get_http_port()}, auth_modes=['basic'], allow_jpeg=True
41 | )
42 | future.add_done_callback(mock_on_response)
43 |
44 | self.wait()
45 | self.assertEqual(1, len(callback_result))
46 | self.assertIsNone(callback_result[0][0])
47 | self.assertEqual('not a supported network camera', callback_result[0][1])
48 |
49 | def test_test_mjpeg_url_jpeg_cam(self):
50 | self.data = 'image/jpeg camera'
51 | callback_result = []
52 |
53 | def mock_on_response(future: Future) -> None:
54 | resp = future.result()
55 | self.stop()
56 | callback_result.append((resp.cameras, resp.error))
57 |
58 | future = test_mjpeg_url(
59 | {'port': self.get_http_port()}, auth_modes=['basic'], allow_jpeg=True
60 | )
61 | future.add_done_callback(mock_on_response)
62 |
63 | self.wait()
64 | self.assertEqual(1, len(callback_result))
65 | self.assertIsNone(callback_result[0][1])
66 |
67 | cams = callback_result[0][0]
68 | self.assertEqual(1, len(cams))
69 |
70 | cam = cams[0]
71 | self.assertDictEqual(
72 | {'id': 1, 'name': 'JPEG Network Camera', 'keep_alive': True}, cam
73 | )
74 |
75 | def test_test_mjpeg_url_mjpeg_cam(self):
76 | self.data = 'mjpeg camera'
77 | callback_result = []
78 |
79 | def mock_on_response(future: Future) -> None:
80 | resp = future.result()
81 | self.stop()
82 | callback_result.append((resp.cameras, resp.error))
83 |
84 | future = test_mjpeg_url(
85 | {'port': self.get_http_port()}, auth_modes=['basic'], allow_jpeg=True
86 | )
87 | future.add_done_callback(mock_on_response)
88 |
89 | self.wait()
90 | self.assertEqual(1, len(callback_result))
91 | self.assertIsNone(callback_result[0][1])
92 |
93 | cams = callback_result[0][0]
94 | self.assertEqual(1, len(cams))
95 |
96 | cam = cams[0]
97 | self.assertDictEqual(
98 | {'id': 1, 'name': 'MJPEG Network Camera', 'keep_alive': True}, cam
99 | )
100 |
101 |
102 | if __name__ == '__main__':
103 | tornado.testing.main()
104 |
--------------------------------------------------------------------------------
/tests/test_utils/test_rtmp.py:
--------------------------------------------------------------------------------
1 | import tornado.testing
2 |
3 | from motioneye.utils.rtmp import test_rtmp_url
4 |
5 |
6 | class UtilsRtmpTest(tornado.testing.AsyncTestCase):
7 | def test_test_rtmp_url(self):
8 | result = test_rtmp_url({})
9 | self.assertEqual([{'id': 'tcp', 'name': 'RTMP/TCP Camera'}], result.cameras)
10 | self.assertIsNone(result.error)
11 |
12 |
13 | if __name__ == '__main__':
14 | tornado.testing.main()
15 |
--------------------------------------------------------------------------------