├── .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/widgets/motioneye-project/-/287x66-black.png)
![](https://hosted.weblate.org/widgets/motioneye-project/-/multi-auto.svg)](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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 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 | --------------------------------------------------------------------------------