diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2d028b2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +filename = + *.py, + *.pys +max-line-length = 120 +extend-exclude = + venv/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6eb0cda --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,48 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +version: 2 +updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + time: "08:00" + open-pull-requests-limit: 10 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "08:30" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + time: "09:00" + open-pull-requests-limit: 10 + + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" + time: "09:30" + open-pull-requests-limit: 10 + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + time: "10:00" + open-pull-requests-limit: 10 + + - package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "daily" + time: "10:30" + open-pull-requests-limit: 10 diff --git a/.github/label-actions.yml b/.github/label-actions.yml new file mode 100644 index 0000000..2949601 --- /dev/null +++ b/.github/label-actions.yml @@ -0,0 +1,49 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Configuration for Label Actions - https://github.com/dessant/label-actions + +added: + comment: > + This feature has been added and will be available in the next release. +fixed: + comment: > + This issue has been fixed and will be available in the next release. +invalid:duplicate: + comment: > + :wave: @{issue-author}, this appears to be a duplicate of a pre-existing issue. + close: true + lock: true + unlabel: 'status:awaiting-triage' + +-invalid:duplicate: + reopen: true + unlock: true + +invalid:support: + comment: > + :wave: @{issue-author}, we use the issue tracker exclusively for bug reports. + However, this issue appears to be a support request. Please use our + [Support Center](https://app.lizardbyte.dev/support) for support issues. Thanks. + close: true + lock: true + lock-reason: 'off-topic' + unlabel: 'status:awaiting-triage' + +-invalid:support: + reopen: true + unlock: true + +invalid:template-incomplete: + issues: + comment: > + :wave: @{issue-author}, please edit your issue to complete the template with + all the required info. Your issue will be automatically closed in 5 days if + the template is not completed. Thanks. + prs: + comment: > + :wave: @{issue-author}, please edit your PR to complete the template with + all the required info. Your PR will be automatically closed in 5 days if + the template is not completed. Thanks. diff --git a/.github/pr_release_template.md b/.github/pr_release_template.md new file mode 100644 index 0000000..b6f6acf --- /dev/null +++ b/.github/pr_release_template.md @@ -0,0 +1,28 @@ +## Description + +This PR was created automatically. + + +### Screenshot + + + +### Issues Fixed or Closed + + + + + +## Type of Change +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Dependency update (updates to dependencies) +- [ ] Documentation update (changes to documentation) +- [ ] Repository update (changes to repository files, e.g. `.github/...`) + +## Branch Updates +- [x] I want maintainers to keep my branch updated + +## Changelog Summary + diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..26def5f --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,128 @@ +--- +name: CI + +on: + pull_request: + branches: [master] + types: [opened, synchronize, reopened] + push: + branches: [master] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + setup_release: + name: Setup Release + outputs: + changelog_changes: ${{ steps.setup_release.outputs.changelog_changes }} + changelog_date: ${{ steps.setup_release.outputs.changelog_date }} + changelog_exists: ${{ steps.setup_release.outputs.changelog_exists }} + changelog_release_exists: ${{ steps.setup_release.outputs.changelog_release_exists }} + changelog_url: ${{ steps.setup_release.outputs.changelog_url }} + changelog_version: ${{ steps.setup_release.outputs.changelog_version }} + publish_pre_release: ${{ steps.setup_release.outputs.publish_pre_release }} + publish_release: ${{ steps.setup_release.outputs.publish_release }} + publish_stable_release: ${{ steps.setup_release.outputs.publish_stable_release }} + release_body: ${{ steps.setup_release.outputs.release_body }} + release_build: ${{ steps.setup_release.outputs.release_build }} + release_commit: ${{ steps.setup_release.outputs.release_commit }} + release_generate_release_notes: ${{ steps.setup_release.outputs.release_generate_release_notes }} + release_tag: ${{ steps.setup_release.outputs.release_tag }} + release_version: ${{ steps.setup_release.outputs.release_version }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Release + id: setup_release + uses: LizardByte/setup-release-action@v2023.1210.1904 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + build: + env: + KODI_BRANCH: Nexus + KODI_PYTHON_VERSION: '3.8' # kodi uses 3.8? https://kodi.wiki/view/Python_libraries + needs: + - setup_release + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.KODI_PYTHON_VERSION }} + + - name: Install python dependencies + shell: bash + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install -r requirements-dev.txt + python -m pip install -r requirements.txt + + - name: Compile Locale Translations + shell: bash + run: | + python -m scripts.locale --compile + + - name: Build + shell: bash + env: + BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }} + run: | + python -m scripts.build + + - name: Package Release + shell: bash + run: | + 7z \ + a "./service.themerr.zip" "service.themerr" + + mkdir artifacts + mv ./service.themerr.zip ./artifacts/ + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: service.themerr + if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` + path: | + ${{ github.workspace }}/artifacts + + - name: Test with pytest + id: test + shell: bash + run: | + python -m pytest \ + -rxXs \ + --tb=native \ + --verbose \ + --cov=src \ + tests + + - name: Upload coverage + # any except canceled or skipped + if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + uses: codecov/codecov-action@v3 + + - name: Create/Update GitHub Release + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} + uses: LizardByte/create-release-action@v2023.1210.832 + with: + allowUpdates: true + body: '' + discussionCategory: announcements + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: ${{ needs.setup_release.outputs.publish_pre_release }} + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/auto-create-pr.yml b/.github/workflows/auto-create-pr.yml new file mode 100644 index 0000000..13705dd --- /dev/null +++ b/.github/workflows/auto-create-pr.yml @@ -0,0 +1,35 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow creates a PR automatically when anything is merged/pushed into the `nightly` branch. The PR is created +# against the `master` (default) branch. + +name: Auto create PR + +on: + push: + branches: + - 'nightly' + +jobs: + create_pr: + if: startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Pull Request + uses: repo-sync/pull-request@v2 + with: + source_branch: "" # should be "nightly" as it's the triggering branch + destination_branch: "master" + pr_title: "Pulling ${{ github.ref_name }} into master" + pr_template: ".github/pr_release_template.md" + pr_assignee: "${{ secrets.GH_BOT_NAME }}" + pr_draft: true + pr_allow_empty: false + github_token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..733b4de --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,64 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow will, first, automatically approve PRs created by @LizardByte-bot. Then it will automerge relevant PRs. + +name: Automerge PR + +on: + pull_request: + types: + - opened + - synchronize + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + autoapprove: + if: >- + contains(fromJson('["LizardByte-bot"]'), github.event.pull_request.user.login) && + contains(fromJson('["LizardByte-bot"]'), github.actor) && + startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + steps: + - name: Autoapproving + uses: hmarr/auto-approve-action@v3 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Label autoapproved + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GH_BOT_TOKEN }} + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['autoapproved', 'autoupdate'] + }) + + automerge: + if: startsWith(github.repository, 'LizardByte/') + needs: [autoapprove] + runs-on: ubuntu-latest + + steps: + - name: Automerging + uses: pascalgn/automerge-action@v0.15.6 + env: + BASE_BRANCHES: nightly + GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + GITHUB_LOGIN: ${{ secrets.GH_BOT_NAME }} + MERGE_LABELS: "!dependencies" + MERGE_METHOD: "squash" + MERGE_COMMIT_MESSAGE: "{pullRequest.title} (#{pullRequest.number})" + MERGE_DELETE_BRANCH: true + MERGE_ERROR_FAIL: true + MERGE_FILTER_AUTHOR: ${{ secrets.GH_BOT_NAME }} + MERGE_RETRIES: "240" # 1 hour + MERGE_RETRY_SLEEP: "15000" # 15 seconds diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml new file mode 100644 index 0000000..ff566fa --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,377 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow is intended to work with all our organization Docker projects. A readme named `DOCKER_README.md` +# will be used to update the description on Docker hub. + +# custom comments in dockerfiles: + +# `# platforms: ` +# Comma separated list of platforms, i.e. `# platforms: linux/386,linux/amd64`. Docker platforms can alternatively +# be listed in a file named `.docker_platforms`. +# `# platforms_pr: ` +# Comma separated list of platforms to run for PR events, i.e. `# platforms_pr: linux/amd64`. This will take +# precedence over the `# platforms: ` directive. +# `# artifacts: ` +# `true` to build in two steps, stopping at `artifacts` build stage and extracting the image from there to the +# GitHub runner. + +name: CI Docker + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, reopened] + push: + branches: [master, nightly] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check_dockerfiles: + name: Check Dockerfiles + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Find dockerfiles + id: find + run: | + dockerfiles=$(find . -type f -iname "Dockerfile" -o -iname "*.dockerfile") + + echo "found dockerfiles: ${dockerfiles}" + + # do not quote to keep this as a single line + echo dockerfiles=${dockerfiles} >> $GITHUB_OUTPUT + + MATRIX_COMBINATIONS="" + for FILE in ${dockerfiles}; do + # extract tag from file name + tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(Dockerfile)/None/gm') + if [[ $tag == "None" ]]; then + MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\"}," + else + tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(.+)(\.dockerfile)/-\2/gm') + MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\", \"tag\": \"$tag\"}," + fi + done + + # removes the last character (i.e. comma) + MATRIX_COMBINATIONS=${MATRIX_COMBINATIONS::-1} + + # setup matrix for later jobs + matrix=$(( + echo "{ \"include\": [$MATRIX_COMBINATIONS] }" + ) | jq -c .) + + echo $matrix + echo $matrix | jq . + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + outputs: + dockerfiles: ${{ steps.find.outputs.dockerfiles }} + matrix: ${{ steps.find.outputs.matrix }} + + setup_release: + if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} + name: Setup Release + needs: + - check_dockerfiles + outputs: + changelog_changes: ${{ steps.setup_release.outputs.changelog_changes }} + changelog_date: ${{ steps.setup_release.outputs.changelog_date }} + changelog_exists: ${{ steps.setup_release.outputs.changelog_exists }} + changelog_release_exists: ${{ steps.setup_release.outputs.changelog_release_exists }} + changelog_url: ${{ steps.setup_release.outputs.changelog_url }} + changelog_version: ${{ steps.setup_release.outputs.changelog_version }} + publish_pre_release: ${{ steps.setup_release.outputs.publish_pre_release }} + publish_release: ${{ steps.setup_release.outputs.publish_release }} + publish_stable_release: ${{ steps.setup_release.outputs.publish_stable_release }} + release_body: ${{ steps.setup_release.outputs.release_body }} + release_build: ${{ steps.setup_release.outputs.release_build }} + release_commit: ${{ steps.setup_release.outputs.release_commit }} + release_generate_release_notes: ${{ steps.setup_release.outputs.release_generate_release_notes }} + release_tag: ${{ steps.setup_release.outputs.release_tag }} + release_version: ${{ steps.setup_release.outputs.release_version }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Release + id: setup_release + uses: LizardByte/setup-release-action@v2023.1210.1904 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + lint_dockerfile: + needs: [check_dockerfiles] + if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }} + name: Lint Dockerfile${{ matrix.tag }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Hadolint + id: hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ${{ matrix.dockerfile }} + ignore: DL3008,DL3013,DL3016,DL3018,DL3028,DL3059 + output-file: ./hadolint.log + verbose: true + + - name: Log + if: failure() + run: | + echo "Hadolint outcome: ${{ steps.hadolint.outcome }}" >> $GITHUB_STEP_SUMMARY + cat "./hadolint.log" >> $GITHUB_STEP_SUMMARY + + docker: + needs: [check_dockerfiles, setup_release] + if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} + runs-on: ubuntu-latest + permissions: + packages: write + contents: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }} + name: Docker${{ matrix.tag }} + + steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v8 + with: + root-reserve-mb: 30720 # https://github.com/easimon/maximize-build-space#caveats + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'true' + + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Prepare + id: prepare + env: + NV: ${{ needs.setup_release.outputs.release_tag }} + run: | + # get branch name + BRANCH=${GITHUB_HEAD_REF} + + RELEASE=${{ needs.setup_release.outputs.publish_release }} + COMMIT=${{ needs.setup_release.outputs.release_commit }} + + if [ -z "$BRANCH" ]; then + echo "This is a PUSH event" + BRANCH=${{ github.ref_name }} + CLONE_URL=${{ github.event.repository.clone_url }} + else + echo "This is a PULL REQUEST event" + CLONE_URL=${{ github.event.pull_request.head.repo.clone_url }} + fi + + # determine to push image to dockerhub and ghcr or not + if [[ $GITHUB_EVENT_NAME == "push" ]]; then + PUSH=true + else + PUSH=false + fi + + # setup the tags + REPOSITORY=${{ github.repository }} + BASE_TAG=$(echo $REPOSITORY | tr '[:upper:]' '[:lower:]') + + TAGS="${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }}" + + if [[ $GITHUB_REF == refs/heads/master ]]; then + TAGS="${TAGS},${BASE_TAG}:latest${{ matrix.tag }},ghcr.io/${BASE_TAG}:latest${{ matrix.tag }}" + TAGS="${TAGS},${BASE_TAG}:master${{ matrix.tag }},ghcr.io/${BASE_TAG}:master${{ matrix.tag }}" + elif [[ $GITHUB_REF == refs/heads/nightly ]]; then + TAGS="${TAGS},${BASE_TAG}:nightly${{ matrix.tag }},ghcr.io/${BASE_TAG}:nightly${{ matrix.tag }}" + else + TAGS="${TAGS},${BASE_TAG}:test${{ matrix.tag }},ghcr.io/${BASE_TAG}:test${{ matrix.tag }}" + fi + + if [[ ${NV} != "" ]]; then + TAGS="${TAGS},${BASE_TAG}:${NV}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${NV}${{ matrix.tag }}" + fi + + # parse custom directives out of dockerfile + # try to get the platforms from the dockerfile custom directive, i.e. `# platforms: xxx,yyy` + # directives for PR event, i.e. not push event + if [[ ${RELEASE} == "false" ]]; then + while read -r line; do + if [[ $line == "# platforms_pr: "* && $PLATFORMS == "" ]]; then + # echo the line and use `sed` to remove the custom directive + PLATFORMS=$(echo -e "$line" | sed 's/# platforms_pr: //') + elif [[ $PLATFORMS != "" ]]; then + # break while loop once all custom "PR" event directives are found + break + fi + done <"${{ matrix.dockerfile }}" + fi + # directives for all events... above directives will not be parsed if they were already found + while read -r line; do + if [[ $line == "# platforms: "* && $PLATFORMS == "" ]]; then + # echo the line and use `sed` to remove the custom directive + PLATFORMS=$(echo -e "$line" | sed 's/# platforms: //') + elif [[ $line == "# artifacts: "* && $ARTIFACTS == "" ]]; then + # echo the line and use `sed` to remove the custom directive + ARTIFACTS=$(echo -e "$line" | sed 's/# artifacts: //') + elif [[ $line == "# no-cache-filters: "* && $NO_CACHE_FILTERS == "" ]]; then + # echo the line and use `sed` to remove the custom directive + NO_CACHE_FILTERS=$(echo -e "$line" | sed 's/# no-cache-filters: //') + elif [[ $PLATFORMS != "" && $ARTIFACTS != "" && $NO_CACHE_FILTERS != "" ]]; then + # break while loop once all custom directives are found + break + fi + done <"${{ matrix.dockerfile }}" + # if PLATFORMS is blank, fall back to the legacy method of reading from the `.docker_platforms` file + if [[ $PLATFORMS == "" ]]; then + # read the platforms from `.docker_platforms` + PLATFORMS=$(<.docker_platforms) + fi + # if PLATFORMS is still blank, fall back to `linux/amd64` + if [[ $PLATFORMS == "" ]]; then + PLATFORMS="linux/amd64" + fi + + echo "branch=${BRANCH}" >> $GITHUB_OUTPUT + echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "clone_url=${CLONE_URL}" >> $GITHUB_OUTPUT + echo "artifacts=${ARTIFACTS}" >> $GITHUB_OUTPUT + echo "no_cache_filters=${NO_CACHE_FILTERS}" >> $GITHUB_OUTPUT + echo "platforms=${PLATFORMS}" >> $GITHUB_OUTPUT + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + + - name: Set Up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + id: buildx + + - name: Cache Docker Layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: Docker-buildx${{ matrix.tag }}-${{ github.sha }} + restore-keys: | + Docker-buildx${{ matrix.tag }}- + + - name: Log in to Docker Hub + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} # PRs do not have access to secrets + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Log in to the Container registry + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} # PRs do not have access to secrets + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ secrets.GH_BOT_NAME }} + password: ${{ secrets.GH_BOT_TOKEN }} + + - name: Build artifacts + if: ${{ steps.prepare.outputs.artifacts == 'true' }} + id: build_artifacts + uses: docker/build-push-action@v5 + with: + context: ./ + file: ${{ matrix.dockerfile }} + target: artifacts + outputs: type=local,dest=artifacts + push: false + platforms: ${{ steps.prepare.outputs.platforms }} + build-args: | + BRANCH=${{ steps.prepare.outputs.branch }} + BUILD_DATE=${{ steps.prepare.outputs.build_date }} + BUILD_VERSION=${{ needs.setup_release.outputs.release_tag }} + COMMIT=${{ needs.setup_release.outputs.release_commit }} + CLONE_URL=${{ steps.prepare.outputs.clone_url }} + RELEASE=${{ needs.setup_release.outputs.publish_release }} + tags: ${{ steps.prepare.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }} + + - name: Build and push + id: build + uses: docker/build-push-action@v5 + with: + context: ./ + file: ${{ matrix.dockerfile }} + push: ${{ needs.setup_release.outputs.publish_release }} + platforms: ${{ steps.prepare.outputs.platforms }} + build-args: | + BRANCH=${{ steps.prepare.outputs.branch }} + BUILD_DATE=${{ steps.prepare.outputs.build_date }} + BUILD_VERSION=${{ needs.setup_release.outputs.release_tag }} + COMMIT=${{ needs.setup_release.outputs.release_commit }} + CLONE_URL=${{ steps.prepare.outputs.clone_url }} + RELEASE=${{ needs.setup_release.outputs.publish_release }} + tags: ${{ steps.prepare.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }} + + - name: Arrange Artifacts + if: ${{ steps.prepare.outputs.artifacts == 'true' }} + working-directory: artifacts + run: | + # artifacts will be in sub directories named after the docker target platform, e.g. `linux_amd64` + # so move files to the artifacts directory + # https://unix.stackexchange.com/a/52816 + find ./ -type f -exec mv -t ./ -n '{}' + + + # remove provenance file + rm -f ./provenance.json + + - name: Upload Artifacts + if: ${{ steps.prepare.outputs.artifacts == 'true' }} + uses: actions/upload-artifact@v3 + with: + name: Docker${{ matrix.tag }} + path: artifacts/ + + - name: Create/Update GitHub Release + if: ${{ needs.setup_release.outputs.publish_release == 'true' && steps.prepare.outputs.artifacts == 'true' }} + uses: LizardByte/create-release-action@v2023.1210.832 + with: + allowUpdates: true + artifacts: "*artifacts/*" + body: '' + discussionCategory: announcements + generateReleaseNotes: true + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: ${{ needs.setup_release.outputs.publish_pre_release }} + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} + + - name: Update Docker Hub Description + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} # token is not currently supported + repository: ${{ env.BASE_TAG }} + short-description: ${{ github.event.repository.description }} + readme-filepath: ./DOCKER_README.md diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..ae52487 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,147 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# This workflow will analyze all supported languages in the repository using CodeQL Analysis. + +name: "CodeQL" + +on: + push: + branches: ["master", "nightly"] + pull_request: + branches: ["master", "nightly"] + schedule: + - cron: '00 12 * * 0' # every Sunday at 12:00 UTC + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + languages: + name: Get language matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.lang.outputs.result }} + continue: ${{ steps.continue.outputs.result }} + steps: + - name: Get repo languages + uses: actions/github-script@v7 + id: lang + with: + script: | + // CodeQL supports ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] + // Use only 'java' to analyze code written in Java, Kotlin or both + // Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + // Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + const supported_languages = ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] + + const remap_languages = { + 'c++': 'cpp', + 'c#': 'csharp', + 'kotlin': 'java', + 'typescript': 'javascript', + } + + const repo = context.repo + const response = await github.rest.repos.listLanguages(repo) + let matrix = { + "include": [] + } + + for (let [key, value] of Object.entries(response.data)) { + // remap language + if (remap_languages[key.toLowerCase()]) { + console.log(`Remapping language: ${key} to ${remap_languages[key.toLowerCase()]}`) + key = remap_languages[key.toLowerCase()] + } + if (supported_languages.includes(key.toLowerCase()) && + !matrix['include'].includes({"language": key.toLowerCase()})) { + console.log(`Found supported language: ${key}`) + matrix['include'].push({"language": key.toLowerCase()}) + } + } + + // print languages + console.log(`matrix: ${JSON.stringify(matrix)}`) + + return matrix + + - name: Continue + uses: actions/github-script@v7 + id: continue + with: + script: | + // if matrix['include'] is an empty list return false, otherwise true + const matrix = ${{ steps.lang.outputs.result }} // this is already json encoded + + if (matrix['include'].length == 0) { + return false + } else { + return true + } + + analyze: + name: Analyze + if: ${{ needs.languages.outputs.continue == 'true' }} + needs: [languages] + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.languages.outputs.matrix) }} + + steps: + - name: Maximize build space + uses: easimon/maximize-build-space@v8 + with: + root-reserve-mb: 20480 + remove-dotnet: ${{ (matrix.language == 'csharp' && 'false') || 'true' }} + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'false' + remove-docker-images: 'true' + + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # yamllint disable-line rule:line-length + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Pre autobuild + # create a file named .codeql-prebuild-${{ matrix.language }}.sh in the root of your repository + - name: Prebuild + run: | + # check if .qodeql-prebuild-${{ matrix.language }}.sh exists + if [ -f "./.codeql-prebuild-${{ matrix.language }}.sh" ]; then + echo "Running .codeql-prebuild-${{ matrix.language }}.sh" + ./.codeql-prebuild-${{ matrix.language }}.sh + fi + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml new file mode 100644 index 0000000..deb3d74 --- /dev/null +++ b/.github/workflows/issues-stale.yml @@ -0,0 +1,61 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Manage stale issues and PRs. + +name: Stale Issues / PRs + +on: + schedule: + - cron: '00 10 * * *' + +jobs: + stale: + name: Check Stale Issues / PRs + if: startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + steps: + - name: Stale + uses: actions/stale@v9 + with: + close-issue-message: > + This issue was closed because it has been stalled for 10 days with no activity. + close-pr-message: > + This PR was closed because it has been stalled for 10 days with no activity. + days-before-stale: 90 + days-before-close: 10 + exempt-all-assignees: true + exempt-issue-labels: 'added,fixed' + exempt-pr-labels: 'dependencies,l10n' + stale-issue-label: 'stale' + stale-issue-message: > + It seems this issue hasn't had any activity in the past 90 days. + If it's still something you'd like addressed, please let us know by leaving a comment. + Otherwise, to help keep our backlog tidy, we'll be closing this issue in 10 days. Thanks! + stale-pr-label: 'stale' + stale-pr-message: > + It looks like this PR has been idle for 90 days. + If it's still something you're working on or would like to pursue, + please leave a comment or update your branch. + Otherwise, we'll be closing this PR in 10 days to reduce our backlog. Thanks! + repo-token: ${{ secrets.GH_BOT_TOKEN }} + + - name: Invalid Template + uses: actions/stale@v9 + with: + close-issue-message: > + This issue was closed because the the template was not completed after 5 days. + close-pr-message: > + This PR was closed because the the template was not completed after 5 days. + days-before-stale: 0 + days-before-close: 5 + only-labels: 'invalid:template-incomplete' + stale-issue-label: 'invalid:template-incomplete' + stale-issue-message: > + Invalid issues template. + stale-pr-label: 'invalid:template-incomplete' + stale-pr-message: > + Invalid PR template. + repo-token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 0000000..aec6006 --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,25 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Label and un-label actions using `../label-actions.yml`. + +name: Issues + +on: + issues: + types: [labeled, unlabeled] + discussion: + types: [labeled, unlabeled] + +jobs: + label: + name: Label Actions + if: startsWith(github.repository, 'LizardByte/') + runs-on: ubuntu-latest + steps: + - name: Label Actions + uses: dessant/label-actions@v4 + with: + github-token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml new file mode 100644 index 0000000..a1bfcd9 --- /dev/null +++ b/.github/workflows/localize.yml @@ -0,0 +1,78 @@ +--- +name: localize + +on: + push: + branches: [master] + paths: # prevents workflow from running unless these files change + - '.github/workflows/localize.yml' + - 'locale/themerr-jellyfin.po' + - 'src/**.py' + workflow_dispatch: + +jobs: + localize: + name: Update Localization + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.8' + + - name: Set up Python Dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install -r requirements-dev.txt + + - name: Update Strings + run: | + python ./scripts/_locale.py --extract + + - name: git diff + run: | + # disable the pager + git config --global pager.diff false + + # print the git diff + git diff Contents/Strings/themerr-plex.po + + # set the variable with minimal output, replacing `\t` with ` ` + OUTPUT=$(git diff --numstat Contents/Strings/themerr-plex.po | sed -e "s#\t# #g") + echo "git_diff=${OUTPUT}" >> $GITHUB_ENV + + - name: git reset + if: ${{ env.git_diff == '1 1 Contents/Strings/themerr-plex.po' }} # only run if more than 1 line changed + run: | + git reset --hard + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Create/Update Pull Request + uses: peter-evans/create-pull-request@v5 + with: + add-paths: | + Contents/Strings/*.po + token: ${{ secrets.GH_BOT_TOKEN }} # must trigger PR tests + commit-message: New localization template + branch: localize/update + delete-branch: true + base: master + title: New Babel Updates + body: | + Update report + - Updated ${{ steps.date.outputs.date }} + - Auto-generated by [create-pull-request][1] + + [1]: https://github.com/peter-evans/create-pull-request + labels: | + babel + l10n diff --git a/.github/workflows/python-flake8.yml b/.github/workflows/python-flake8.yml new file mode 100644 index 0000000..e08ab10 --- /dev/null +++ b/.github/workflows/python-flake8.yml @@ -0,0 +1,38 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Lint python files with flake8. + +name: flake8 + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + flake8: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 # https://github.com/actions/setup-python + with: + python-version: '3.10' + + - name: Install dependencies + run: | + # pin flake8 before v6.0.0 due to removal of support for type comments (required for Python 2.7 type hints) + python -m pip install --upgrade pip setuptools "flake8<6" + + - name: Test with flake8 + run: | + python -m flake8 --verbose diff --git a/.github/workflows/release-notifier.yml b/.github/workflows/release-notifier.yml new file mode 100644 index 0000000..5735465 --- /dev/null +++ b/.github/workflows/release-notifier.yml @@ -0,0 +1,103 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Send release notification to various platforms. + +name: Release Notifications + +on: + release: + types: [published] + # https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onevent_nametypes + +jobs: + discord: + if: >- + startsWith(github.repository, 'LizardByte/') && + not(github.event.release.prerelease) && + not(github.event.release.draft) + runs-on: ubuntu-latest + steps: + - name: discord + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} + nodetail: true + nofail: false + username: ${{ secrets.DISCORD_USERNAME }} + avatar_url: ${{ secrets.ORG_LOGO_URL }} + title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released + description: ${{ github.event.release.body }} + color: 0xFF4500 + + facebook_group: + if: >- + startsWith(github.repository, 'LizardByte/') && + not(github.event.release.prerelease) && + not(github.event.release.draft) + runs-on: ubuntu-latest + steps: + - name: facebook-post-action + uses: ReenigneArcher/facebook-post-action@v1 + with: + page_id: ${{ secrets.FACEBOOK_GROUP_ID }} + access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} + message: | + ${{ github.event.repository.name }} ${{ github.ref_name }} Released + ${{ github.event.release.body }} + url: ${{ github.event.release.html_url }} + + facebook_page: + if: >- + startsWith(github.repository, 'LizardByte/') && + not(github.event.release.prerelease) && + not(github.event.release.draft) + runs-on: ubuntu-latest + steps: + - name: facebook-post-action + uses: ReenigneArcher/facebook-post-action@v1 + with: + page_id: ${{ secrets.FACEBOOK_PAGE_ID }} + access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} + message: | + ${{ github.event.repository.name }} ${{ github.ref_name }} Released + ${{ github.event.release.body }} + url: ${{ github.event.release.html_url }} + + reddit: + if: >- + startsWith(github.repository, 'LizardByte/') && + not(github.event.release.prerelease) && + not(github.event.release.draft) + runs-on: ubuntu-latest + steps: + - name: reddit + uses: bluwy/release-for-reddit-action@v2 + with: + username: ${{ secrets.REDDIT_USERNAME }} + password: ${{ secrets.REDDIT_PASSWORD }} + app-id: ${{ secrets.REDDIT_CLIENT_ID }} + app-secret: ${{ secrets.REDDIT_CLIENT_SECRET }} + subreddit: ${{ secrets.REDDIT_SUBREDDIT }} + title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released + url: ${{ github.event.release.html_url }} + flair-id: ${{ secrets.REDDIT_FLAIR_ID }} # https://www.reddit.com/r/>/api/link_flair.json + comment: ${{ github.event.release.body }} + + twitter: + if: >- + startsWith(github.repository, 'LizardByte/') && + not(github.event.release.prerelease) && + not(github.event.release.draft) + runs-on: ubuntu-latest + steps: + - name: twitter + uses: nearform-actions/github-action-notify-twitter@v1 + with: + message: ${{ github.event.release.html_url }} + twitter-app-key: ${{ secrets.TWITTER_API_KEY }} + twitter-app-secret: ${{ secrets.TWITTER_API_SECRET }} + twitter-access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} + twitter-access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml new file mode 100644 index 0000000..7e1fd46 --- /dev/null +++ b/.github/workflows/yaml-lint.yml @@ -0,0 +1,66 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Lint yaml files. + +name: yaml lint + +on: + pull_request: + branches: [master, nightly] + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + yaml-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Find additional files + id: find-files + run: | + # space separated list of files + FILES=.clang-format + + # empty placeholder + FOUND="" + + for FILE in ${FILES}; do + if [ -f "$FILE" ] + then + FOUND="$FOUND $FILE" + fi + done + + echo "found=${FOUND}" >> $GITHUB_OUTPUT + + - name: yaml lint + id: yaml-lint + uses: ibiqlik/action-yamllint@v3 + with: + # https://yamllint.readthedocs.io/en/stable/configuration.html#default-configuration + config_data: | + extends: default + rules: + comments: + level: error + line-length: + max: 120 + truthy: + # GitHub uses "on" for workflow event triggers + # .clang-format file has options of "Yes" "No" that will be caught by this, so changed to "warning" + allowed-values: ['true', 'false', 'on'] + check-keys: true + level: warning + file_or_dir: . ${{ steps.find-files.outputs.found }} + + - name: Log + run: | + cat "${{ steps.yaml-lint.outputs.logfile }}" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 68bc17f..be641b4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +#lib/ lib64/ parts/ sdist/ @@ -157,4 +157,8 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +# addon build +service.themerr/ +*.zip diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b338a87 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "third-party/youtube-dl"] + path = third-party/youtube-dl + url = https://github.com/ytdl-org/youtube-dl.git + branch = master diff --git a/README.md b/README.md new file mode 100644 index 0000000..23fa870 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Themerr-kodi + +This is a WIP addon. Sphinx docs will be added later. + +For now, these are just some notes. + +#### compile translations +```bash +python -m scripts.locale --compile +``` + +#### build plugin +```bash +python -m scripts.build +``` + +#### service.tvtunes addon reference +https://github.com/latts9923/service.tvtunes + +#### Problems installing addon +- [Common errors](https://kodi.wiki/view/Addon.xml#Common_errors) diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..0be504b --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,22 @@ +--- +"base_path": "." +"base_url": "https://api.crowdin.com" # optional (for Crowdin Enterprise only) +"preserve_hierarchy": false # flatten tree on crowdin +"pull_request_labels": [ + "crowdin", + "l10n" +] + +"files": [ + { + "source": "/locale/*.po", + "translation": "/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%", + "languages_mapping": { + "two_letters_code": { + # map non-two letter codes here, left side is crowdin designation, right side is babel designation + "en-GB": "en_GB", + "en-US": "en_US" + } + } + } +] diff --git a/locale/de/LC_MESSAGES/themerr-jellyfin.po b/locale/de/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..e4d2b50 --- /dev/null +++ b/locale/de/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,21 @@ +# German translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: de\n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/en/LC_MESSAGES/themerr-jellyfin.po b/locale/en/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..c362754 --- /dev/null +++ b/locale/en/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,21 @@ +# English translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: en\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/en_GB/LC_MESSAGES/themerr-jellyfin.po b/locale/en_GB/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..3e961b7 --- /dev/null +++ b/locale/en_GB/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,21 @@ +# English (United Kingdom) translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: en_GB\n" +"Language-Team: en_GB \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/en_US/LC_MESSAGES/themerr-jellyfin.po b/locale/en_US/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..26581d6 --- /dev/null +++ b/locale/en_US/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,21 @@ +# English (United States) translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: en_US\n" +"Language-Team: en_US \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/es/LC_MESSAGES/themerr-jellyfin.po b/locale/es/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..7fc6695 --- /dev/null +++ b/locale/es/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,21 @@ +# Spanish translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: es\n" +"Language-Team: es \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/fr/LC_MESSAGES/themerr-jellyfin.po b/locale/fr/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..ddd52de --- /dev/null +++ b/locale/fr/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,21 @@ +# French translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/it/LC_MESSAGES/themerr-jellyfin.po b/locale/it/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..3900d4e --- /dev/null +++ b/locale/it/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,21 @@ +# Italian translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: it\n" +"Language-Team: it \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/ru/LC_MESSAGES/themerr-jellyfin.po b/locale/ru/LC_MESSAGES/themerr-jellyfin.po new file mode 100644 index 0000000..4926104 --- /dev/null +++ b/locale/ru/LC_MESSAGES/themerr-jellyfin.po @@ -0,0 +1,22 @@ +# Russian translations for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: 2023-12-22 17:38-0500\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/locale/themerr-jellyfin.po b/locale/themerr-jellyfin.po new file mode 100644 index 0000000..a9c6514 --- /dev/null +++ b/locale/themerr-jellyfin.po @@ -0,0 +1,20 @@ +# Translations template for Themerr-jellyfin. +# Copyright (C) 2023 Themerr-jellyfin +# This file is distributed under the same license as the Themerr-jellyfin +# project. +# FIRST AUTHOR , 2023. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Themerr-jellyfin v0\n" +"Report-Msgid-Bugs-To: github.com/themerr-jellyfin\n" +"POT-Creation-Date: 2023-12-22 17:40-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..15510c0 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +Babel==2.14.0 +flake8==6.1.0 +kodi-addon-checker==0.0.31 +Kodistubs==20.0.1 # docs: https://romanvm.github.io/Kodistubs +pytest==7.4.3 +pytest-cov==4.1.0 +requests==2.31.0 # todo: decide if we want to use this or the kodi provided one diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..505e2dd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# youtube-dl +# pypi version is broken so use submodule +./third-party/youtube-dl diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/babel.cfg b/scripts/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/scripts/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/scripts/build.py b/scripts/build.py new file mode 100644 index 0000000..95043b1 --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,85 @@ +# standard imports +import os +import shutil +import subprocess +import sys + +# local imports +from src.themerr import constants + +# list of directory to copy contents of +source_dirs = [ + 'src', +] + +# list of files to copy +source_files = [ + 'LICENSE', +] + +script_directory: str = os.path.dirname(os.path.abspath(__file__)) +root_directory: str = os.path.dirname(script_directory) +build_directory: str = os.path.join(root_directory, constants.addon_id) +pip_install_directory: str = os.path.join(build_directory, 'resources', 'lib') + + +def build(): + # create the build directory + try: + os.makedirs(build_directory, exist_ok=False) + except FileExistsError: + # remove the build directory + shutil.rmtree(build_directory) + + # create the build directory + os.makedirs(build_directory, exist_ok=False) + + # copy the source directories, recursively + for directory in source_dirs: + source_directory: str = os.path.join(root_directory, directory) + shutil.copytree( + src=source_directory, + dst=build_directory, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns('*.pyc', '__pycache__'), + ) + + # copy the source files + for file in source_files: + source_file: str = os.path.join(root_directory, file) + destination_file: str = os.path.join(build_directory, file) + + shutil.copy2(source_file, destination_file) + + +def check_addon(): + """ + Run kodi-addon-checker --branch nexus service.themerr in subprocess. + """ + kodi_branch = os.getenv('KODI_BRANCH', 'Nexus').lower() + subprocess.run( + args=['kodi-addon-checker', '--branch', kodi_branch, constants.addon_id], + check=True, # raise called process error if return code is non-zero + ) + + +def install_dependencies(): + """ + Install dependencies in subprocess, using this script's python executable. + """ + # get python executable path + python = sys.executable + + # install dependencies to specified directory + subprocess.run( + args=[python, '-m', 'pip', 'install', '-r', 'requirements.txt', '-t', pip_install_directory], + check=True, # raise called process error if return code is non-zero + ) + + +if __name__ == '__main__': + build() + check_addon() + + # ideally this would be before the check, but kodi-addon-checker tries refactoring everything + install_dependencies() diff --git a/scripts/locale.py b/scripts/locale.py new file mode 100644 index 0000000..6e9a314 --- /dev/null +++ b/scripts/locale.py @@ -0,0 +1,123 @@ +# coding=utf-8 +""" +.. + _locale.py + +Functions related to building, initializing, updating, and compiling localization translations. +""" +# standard imports +import argparse +import os +import subprocess + +project_name = 'Themerr-jellyfin' + +script_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.dirname(script_dir) +locale_dir = os.path.join(root_dir, 'locale') + +# target locales +target_locales = [ + 'de', # Deutsch + 'en', # English + 'en_GB', # English (United Kingdom) + 'en_US', # English (United States) + 'es', # español + 'fr', # français + 'it', # italiano + 'ru', # русский +] + + +def babel_extract(): + """Executes `pybabel extract` in subprocess.""" + commands = [ + 'pybabel', + 'extract', + '-F', os.path.join(script_dir, 'babel.cfg'), + '-o', os.path.join(locale_dir, f'{project_name.lower()}.po'), + '--sort-by-file', + f'--msgid-bugs-address=github.com/{project_name.lower()}', + f'--copyright-holder={project_name}', + f'--project={project_name}', + '--version=v0', + '--add-comments=NOTE', + './src', + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +def babel_init(locale_code): + # type: (str) -> None + """Executes `pybabel init` in subprocess. + + :param locale_code: str - locale code + """ + commands = [ + 'pybabel', + 'init', + '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'), + '-d', locale_dir, + '-D', project_name.lower(), + '-l', locale_code + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +def babel_update(): + """Executes `pybabel update` in subprocess.""" + commands = [ + 'pybabel', + 'update', + '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'), + '-d', locale_dir, + '-D', project_name.lower(), + '--update-header-comment' + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +def babel_compile(): + """Executes `pybabel compile` in subprocess.""" + commands = [ + 'pybabel', + 'compile', + '-d', locale_dir, + '-D', project_name.lower() + ] + + print(commands) + subprocess.check_output(args=commands, cwd=root_dir) + + +if __name__ == '__main__': + # Set up and gather command line arguments + parser = argparse.ArgumentParser( + description='Script helps update locale translations. Translations must be done manually.') + + parser.add_argument('--extract', action='store_true', help='Extract messages from python files and templates.') + parser.add_argument('--init', action='store_true', help='Initialize any new locales specified in target locales.') + parser.add_argument('--update', action='store_true', help='Update existing locales.') + parser.add_argument('--compile', action='store_true', help='Compile translated locales.') + + args = parser.parse_args() + + if args.extract: + babel_extract() + + if args.init: + for locale_id in target_locales: + if not os.path.isdir(os.path.join(locale_dir, locale_id)): + babel_init(locale_code=locale_id) + + if args.update: + babel_update() + + if args.compile: + babel_compile() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/addon.xml b/src/addon.xml new file mode 100644 index 0000000..bb1ed28 --- /dev/null +++ b/src/addon.xml @@ -0,0 +1,27 @@ + + + + + + + resources/assets/images/clearlogo.png + resources/assets/images/icon.png + resources/assets/images/fanart.jpg + resources/assets/images/screenshot-01.jpg + resources/assets/images/banner.jpg + + Plugin for Kodi that adds theme songs to movies using ThemerrDB. + AGPL-3.0-only + all + https://github.com/LizardByte/Themerr-jellyfin + Play theme songs while browsing movies + https://app.lizardbyte.dev/ + + + + + + diff --git a/src/plugin.py b/src/plugin.py new file mode 100644 index 0000000..e69de29 diff --git a/src/resources/assets/images/banner.jpg b/src/resources/assets/images/banner.jpg new file mode 100644 index 0000000..77525a1 Binary files /dev/null and b/src/resources/assets/images/banner.jpg differ diff --git a/src/resources/assets/images/clearlogo.png b/src/resources/assets/images/clearlogo.png new file mode 100644 index 0000000..b25c0ad Binary files /dev/null and b/src/resources/assets/images/clearlogo.png differ diff --git a/src/resources/assets/images/fanart.jpg b/src/resources/assets/images/fanart.jpg new file mode 100644 index 0000000..c14d018 Binary files /dev/null and b/src/resources/assets/images/fanart.jpg differ diff --git a/src/resources/assets/images/icon.png b/src/resources/assets/images/icon.png new file mode 100644 index 0000000..8bb9f00 Binary files /dev/null and b/src/resources/assets/images/icon.png differ diff --git a/src/resources/assets/images/screenshot-01.jpg b/src/resources/assets/images/screenshot-01.jpg new file mode 100644 index 0000000..c14d018 Binary files /dev/null and b/src/resources/assets/images/screenshot-01.jpg differ diff --git a/src/resources/lib/__init__.py b/src/resources/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/service.py b/src/service.py new file mode 100644 index 0000000..48241c8 --- /dev/null +++ b/src/service.py @@ -0,0 +1,49 @@ +# standard imports +import os +import sys + +# kodi imports +import xbmc +import xbmcvfs + +# lib imports +from themerr import constants +from themerr.logger import Logger +from themerr.settings import Settings + +settings = Settings() +log = Logger() + +ADDON = settings.addon +CWD = ADDON.getAddonInfo('path') +LIB_DIR = xbmcvfs.translatePath(os.path.join(CWD, 'resources', 'lib')) + +# add the lib directory to the python path +sys.path.insert(0, LIB_DIR) + + +class ThemerrMonitor(xbmc.Monitor): + """ + Kodi's monitor class. + + Creates a new monitor to notify addon about changes. + """ + def onSettingsChanged(self): + log.debug("ThemerrMonitor: Settings have been modified") + + # reload the settings + global settings + settings = Settings() + + +if __name__ == '__main__': + log.debug(f"Starting {constants.name} Service {ADDON.getAddonInfo('version')}") + + # create a monitor to watch for changes in the addon + system_monitor = ThemerrMonitor() + + # todo: create a ThemerrService class + # run the service + # delete the service + + del system_monitor diff --git a/src/themerr/__init__.py b/src/themerr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/themerr/constants.py b/src/themerr/constants.py new file mode 100644 index 0000000..ae2ebe3 --- /dev/null +++ b/src/themerr/constants.py @@ -0,0 +1,3 @@ +name = "Themerr" +addon_type = "service" +addon_id = f"{addon_type}.{name.lower()}" diff --git a/src/themerr/gui.py b/src/themerr/gui.py new file mode 100644 index 0000000..66f474c --- /dev/null +++ b/src/themerr/gui.py @@ -0,0 +1,87 @@ +# standard imports +from typing import List, Optional + +# kodi imports +import xbmc +import xbmcgui + + +class Window: + def __init__(self): + pass + + @staticmethod + def is_true(check: Optional[bool] = None, checks: Optional[List[bool]] = ()): + """ + Determine if the check is True or if any of the checks are True. + + Parameters + ---------- + check : Optional[bool] + The check to perform. + checks : Optional[List[bool]] + The checks to perform. + + Returns + ------- + bool + True if any of the checks are True, otherwise False. + + Examples + -------- + >>> Window().is_true(checks=[True, False, False]) + True + >>> Window().is_true(checks=[False, False, False]) + False + >>> Window().is_true(check=True) + True + >>> Window().is_true(check=False) + False + """ + if len(checks) == 0: + return check + + for check in checks: + if check: + return True + + def is_home(self): + return self.is_true(check=xbmc.getCondVisibility("Window.IsVisible(home)")) + + def is_movies(self): + return self.is_true(checks=[ + xbmc.getCondVisibility("Container.Content(movies)"), + (xbmc.getInfoLabel("ListItem.dbtype") == 'movie'), + ]) + + @staticmethod + def is_movie_set(): + # i.e. collections + return xbmc.getCondVisibility("ListItem.IsCollection") + + def is_tv_shows(self): + return self.is_true(checks=[ + xbmc.getCondVisibility("Container.Content(tvshows)"), + (xbmc.getInfoLabel("ListItem.dbtype") == 'tvshow'), + ]) + + def is_seasons(self): + return self.is_true(checks=[ + xbmc.getCondVisibility("Container.Content(Seasons)"), + (xbmc.getInfoLabel("ListItem.dbtype") == 'season'), + ]) + + def is_episodes(self): + return self.is_true(checks=[ + xbmc.getCondVisibility("Container.Content(Episodes)"), + (xbmc.getInfoLabel("ListItem.dbtype") == 'episode'), + ]) + + @staticmethod + def is_themerr_override(): + try: + _win = xbmcgui.Window(xbmcgui.getCurrentWindowId()) + except RuntimeError: + return False + else: + return _win.getProperty('ThemerrSupported').lower() == 'true' diff --git a/src/themerr/logger.py b/src/themerr/logger.py new file mode 100644 index 0000000..6b7d8e0 --- /dev/null +++ b/src/themerr/logger.py @@ -0,0 +1,114 @@ +# kodi imports +import xbmc + +# local imports +from . import constants + + +class Logger(object): + """ + Themerr's logger class. + + Creates a new logger to log to the Kodi log. + """ + def __init__(self): + pass + + @staticmethod + def log(msg: str, level: int = xbmc.LOGDEBUG): + """ + Log a message to the Kodi log. + + Parameters + ---------- + msg : str + The message to log. + level : int + The log level to log the message at. + + Examples + -------- + >>> log = Logger() + >>> log.log("This is a debug message", xbmc.LOGDEBUG) + """ + xbmc.log(msg=f"{constants.name}: {msg}", level=level) + + def debug(self, msg: str): + """ + Log a debug message to the Kodi log. + + Parameters + ---------- + msg : str + The message to log. + + Examples + -------- + >>> log = Logger() + >>> log.debug("This is a debug message") + """ + self.log(msg=msg, level=xbmc.LOGDEBUG) + + def info(self, msg: str): + """ + Log an info message to the Kodi log. + + Parameters + ---------- + msg : str + The message to log. + + Examples + -------- + >>> log = Logger() + >>> log.info("This is an info message") + """ + self.log(msg=msg, level=xbmc.LOGINFO) + + def warning(self, msg: str): + """ + Log a warning message to the Kodi log. + + Parameters + ---------- + msg : str + The message to log. + + Examples + -------- + >>> log = Logger() + >>> log.warning("This is a warning message") + """ + self.log(msg=msg, level=xbmc.LOGWARNING) + + def error(self, msg: str): + """ + Log an error message to the Kodi log. + + Parameters + ---------- + msg : str + The message to log. + + Examples + -------- + >>> log = Logger() + >>> log.error("This is an error message") + """ + self.log(msg=msg, level=xbmc.LOGERROR) + + def fatal(self, msg: str): + """ + Log a fatal message to the Kodi log. + + Parameters + ---------- + msg : str + The message to log. + + Examples + -------- + >>> log = Logger() + >>> log.fatal("This is a fatal message") + """ + self.log(msg=msg, level=xbmc.LOGFATAL) diff --git a/src/themerr/settings.py b/src/themerr/settings.py new file mode 100644 index 0000000..22b8266 --- /dev/null +++ b/src/themerr/settings.py @@ -0,0 +1,13 @@ +# kodi imports +import xbmcaddon + +# local imports +from . import constants + + +class Settings: + def __init__(self): + """ + Initialize the Settings class. + """ + self.addon = xbmcaddon.Addon(id=constants.addon_id) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/themerr.png b/themerr.png new file mode 100644 index 0000000..3d00618 Binary files /dev/null and b/themerr.png differ diff --git a/third-party/youtube-dl b/third-party/youtube-dl new file mode 160000 index 0000000..be008e6 --- /dev/null +++ b/third-party/youtube-dl @@ -0,0 +1 @@ +Subproject commit be008e657d79832642e2158557c899249c9e31cd