Compare commits

..

No commits in common. "main" and "3.0.0-beta.2" have entirely different histories.

796 changed files with 38402 additions and 41158 deletions

22
.chglog/CHANGELOG.md.tpl Normal file
View File

@ -0,0 +1,22 @@
{{ range .Versions }}<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }})
- [Release note](https://libretime.org/docs/releases/{{ .Tag.Name }}/)
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range reverse .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
{{ end }}
{{ end -}}
{{- if .RevertCommits -}}
### Reverts
{{ range .RevertCommits -}}
- {{ .Revert.Header }}
{{ end }}
{{ end -}}
{{ end -}}

25
.chglog/config.yml Normal file
View File

@ -0,0 +1,25 @@
style: github
template: CHANGELOG.md.tpl
info:
title: CHANGELOG
repository_url: https://github.com/libretime/libretime
options:
commits:
filters:
Type: [feat, fix, docs, test, ci]
sort_by: Date
commit_groups:
title_maps:
feat: Features
fix: Bug Fixes
docs: Documentation
test: Tests
ci: CI
sort_by: Custom
title_order: [feat, fix, docs, test, ci]
header:
pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
pattern_maps:
- Type
- Scope
- Subject

View File

@ -4,8 +4,6 @@ HDA
ro ro
# Names # Names
conet
falso
flor flor
# TODO: See https://github.com/savonet/liquidsoap/issues/1654 # TODO: See https://github.com/savonet/liquidsoap/issues/1654

View File

@ -1,3 +1,3 @@
LIBRETIME_VERSION=main LIBRETIME_VERSION=main
LIBRETIME_CONFIG_FILEPATH=./dev/config.yml LIBRETIME_CONFIG_FILEPATH=./docker/config.dev.yml
NGINX_CONFIG_FILEPATH=./docker/nginx.conf NGINX_CONFIG_FILEPATH=./docker/nginx.conf

View File

@ -5,6 +5,6 @@ contact_links:
url: https://discourse.libretime.org/ url: https://discourse.libretime.org/
about: Please find existing questions and discussions here. about: Please find existing questions and discussions here.
- name: LibreTime Chat (#libretime:matrix.org) - name: LibreTime Chat
url: https://matrix.to/#/#libretime:matrix.org url: https://chat.libretime.org/
about: Discuss with the LibreTime community. about: Discuss with the LibreTime community.

View File

@ -1,20 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"bootstrap-sha": "26737abad231d96fc198fbf12c043f2d867be79c",
"include-component-in-tag": false,
"include-v-in-tag": false,
"packages": {
".": {
"release-type": "simple",
"package-name": "libretime",
"extra-files": [
"analyzer/setup.py",
"api/setup.py",
"api-client/setup.py",
"playout/setup.py",
"shared/setup.py",
"worker/setup.py"
]
}
}
}

View File

@ -1 +0,0 @@
{".":"4.2.0"}

19
.github/renovate.json vendored
View File

@ -6,11 +6,8 @@
"commitMessageAction": "lock file maintenance", "commitMessageAction": "lock file maintenance",
"commitMessageExtra": "({{packageFile}})", "commitMessageExtra": "({{packageFile}})",
"branchTopic": "lock-file-maintenance-{{packageFile}}", "branchTopic": "lock-file-maintenance-{{packageFile}}",
"schedule": ["after 4am and before 5am on monday"], "schedule": ["after 4am and before 5am on monday"]
"automerge": true,
"automergeType": "branch"
}, },
"baseBranches": ["main"],
"labels": ["dependencies"], "labels": ["dependencies"],
"packageRules": [ "packageRules": [
{ {
@ -27,9 +24,17 @@
"rangeStrategy": "widen" "rangeStrategy": "widen"
}, },
{ {
"matchManagers": ["github-actions", "pre-commit"], "matchPaths": ["website/**"],
"automerge": true, "addLabels": ["javascript"]
"automergeType": "branch" },
{
"matchUpdateTypes": ["patch"],
"matchPaths": [
".github/workflows/*",
".pre-commit-config.yaml",
"website/**"
],
"automerge": true
} }
] ]
} }

57
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,57 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale (5 months)
daysUntilStale: 150
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. (1 month)
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 30
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- "status: pinned"
- "status: maybe later"
- "security"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking as stale
staleLabel: "status: stalled"
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
activity in the last 5 months. It will be closed if no activity occurs in
the next month.
Please chat to us on [discourse](https://discourse.libretime.org/) or
ask for help on our [chat](https://chat.libretime.org/) if you have any
questions or need further support with getting this issue resolved.
You may also label an issue as *pinned* if you would like to make sure
that it does not get closed by this bot.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been automatically closed after is was marked as stale and
did not receive any further inputs.
Feel free to let us know on [discourse](https://discourse.libretime.org/) or
ask for help on our [chat](https://chat.libretime.org/) if you feel this
issue should not have been closed.
Thank you for your contributions.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30

View File

@ -25,11 +25,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: "3.x" python-version: "3.x"
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }} key: ${{ runner.os }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
@ -51,8 +51,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
release: release:
- focal - buster
- bullseye - bullseye
- bionic
- focal
- jammy - jammy
container: container:
@ -63,9 +65,9 @@ jobs:
shell: bash shell: bash
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ matrix.release }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }} key: ${{ matrix.release }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
@ -73,11 +75,11 @@ jobs:
${{ matrix.release }}-pip-${{ inputs.context }} ${{ matrix.release }}-pip-${{ inputs.context }}
- name: Test - name: Test
run: make test-coverage run: make test
working-directory: ${{ inputs.context }} working-directory: ${{ inputs.context }}
- name: Report coverage - name: Report coverage
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v3
with: with:
files: ${{ inputs.context }}/coverage.xml files: ${{ inputs.context }}/coverage.xml
flags: ${{ inputs.context }} flags: ${{ inputs.context }}

View File

@ -0,0 +1,31 @@
name: Build container
description: Build and push a container
inputs:
target:
required: true
runs:
using: composite
steps:
- uses: docker/metadata-action@v4
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ inputs.target }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- uses: docker/build-push-action@v3
with:
context: .
pull: true
push: ${{ github.event_name == 'push' }}
build-args: |
LIBRETIME_VERSION=${{ env.LIBRETIME_VERSION }}
target: ${{ inputs.target }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ inputs.target }}
cache-to: type=gha,scope=${{ inputs.target }},mode=max

View File

@ -1,27 +1,21 @@
name: Analyzer name: Analyzer
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/analyzer.yml - .github/workflows/analyzer.yml
- analyzer/** - analyzer/**
- shared/** - shared/**
- tools/python*
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/analyzer.yml - .github/workflows/analyzer.yml
- analyzer/** - analyzer/**
- shared/** - shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs: jobs:
python: python:

View File

@ -1,27 +1,21 @@
name: API Client name: API Client
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/api-client.yml - .github/workflows/api-client.yml
- api-client/** - api-client/**
- shared/** - shared/**
- tools/python*
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/api-client.yml - .github/workflows/api-client.yml
- api-client/** - api-client/**
- shared/** - shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs: jobs:
python: python:

View File

@ -1,7 +1,6 @@
name: API schema name: API schema
on: on:
workflow_dispatch:
push: push:
branches: [main] branches: [main]
paths: paths:
@ -21,15 +20,15 @@ jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: "3.x" python-version: "3.x"
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-api-${{ hashFiles('api/**/setup.py') }} key: ${{ runner.os }}-pip-api-${{ hashFiles('api/**/setup.py') }}
@ -42,7 +41,7 @@ jobs:
- name: Get pull request commit range - name: Get pull request commit range
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: echo "COMMIT_RANGE=${{ github.sha }}~1...${{ github.sha }}" >> $GITHUB_ENV run: echo "COMMIT_RANGE=origin/${{ github.base_ref }}..${{ github.sha }}" >> $GITHUB_ENV
- name: Get push commit range - name: Get push commit range
if: github.event_name == 'push' if: github.event_name == 'push'
@ -54,11 +53,11 @@ jobs:
git checkout $commit git checkout $commit
make --quiet schema make --quiet schema
git diff -- schema.yml
git add schema.yml git add schema.yml
git diff-index --quiet HEAD -- || { git diff-index --quiet HEAD -- || {
echo "ERROR: Schema is outdated for commit $commit" echo "ERROR: Schema is outdated for commit $commit"
git show --quiet git show --quiet
git diff -- schema.yml
exit 1 exit 1
} }
done done
@ -71,11 +70,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
repository: libretime/client repository: libretime/client
path: client path: client

View File

@ -1,27 +1,21 @@
name: API name: API
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/api.yml - .github/workflows/api.yml
- api/** - api/**
- shared/** - shared/**
- tools/python*
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/api.yml - .github/workflows/api.yml
- api/** - api/**
- shared/** - shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs: jobs:
python: python:
@ -36,8 +30,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
release: release:
- focal - buster
- bullseye - bullseye
- bionic
- focal
- jammy - jammy
services: services:
@ -62,9 +58,9 @@ jobs:
LIBRETIME_DATABASE_HOST: postgres LIBRETIME_DATABASE_HOST: postgres
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ matrix.release }}-pip-api-${{ hashFiles('api/**/setup.py') }} key: ${{ matrix.release }}-pip-api-${{ hashFiles('api/**/setup.py') }}
@ -72,11 +68,11 @@ jobs:
${{ matrix.release }}-pip-api ${{ matrix.release }}-pip-api
- name: Test - name: Test
run: make test-coverage run: make test
working-directory: api working-directory: api
- name: Report coverage - name: Report coverage
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v3
with: with:
files: api/coverage.xml files: api/coverage.xml
flags: api flags: api

View File

@ -1,34 +0,0 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
jobs:
backport:
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: jooola/backport@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
title_template: <%= title %> (<%= base %>)
body_template: |
Backport <%= mergeCommitSha %> from #<%= number %>.
BEGIN_COMMIT_OVERRIDE
<%= title %>
END_COMMIT_OVERRIDE

21
.github/workflows/command.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Command
on:
issue_comment:
types: [created]
jobs:
dispatch:
name: Dispatch
runs-on: ubuntu-latest
steps:
- name: Dispatch website preview
uses: peter-evans/slash-command-dispatch@v3
with:
token: ${{ secrets.COMMAND_DISPATCH_TOKEN }}
issue-type: pull-request
dispatch-type: workflow
commands: website-preview
static-args: |
pull-request-number=${{ github.event.issue.number }}

View File

@ -3,101 +3,86 @@ name: Container
on: on:
push: push:
tags: ["[0-9]+.[0-9]+.[0-9]+*"] tags: ["[0-9]+.[0-9]+.[0-9]+*"]
branches: [main, stable-*] branches: [main]
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
jobs: jobs:
meta:
runs-on: ubuntu-latest
strategy:
matrix:
target: [analyzer, api, legacy, playout, worker]
if: ${{ github.repository_owner == 'libretime' }}
steps:
- uses: actions/checkout@v4
- name: Update Docker Hub description
if: github.event_name == 'push'
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: libretime/libretime-${{ matrix.target }}
readme-filepath: ./README.md
- uses: docker/metadata-action@v5
id: meta
with:
bake-target: ${{ matrix.target }}
images: |
ghcr.io/libretime/libretime-${{ matrix.target }}
docker.io/libretime/libretime-${{ matrix.target }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Upload metadata bake file
uses: actions/upload-artifact@v4
with:
name: meta-${{ matrix.target }}
path: ${{ steps.meta.outputs.bake-file }}
build: build:
needs: [meta]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
NAMESPACE: ${{ github.repository_owner }}
if: ${{ github.repository_owner == 'libretime' }} if: ${{ github.repository_owner == 'libretime' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v2
- name: Login ghcr.io - uses: docker/login-action@v2
if: github.event_name == 'push'
uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login docker.io
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Download all metadata bake files
uses: actions/download-artifact@v4
with:
pattern: meta-*
- name: Guess LIBRETIME_VERSION - name: Guess LIBRETIME_VERSION
run: | run: |
make VERSION make VERSION
echo "LIBRETIME_VERSION=$(cat VERSION | tr -d [:blank:])" >> $GITHUB_ENV echo "LIBRETIME_VERSION=$(cat VERSION | tr -d [:blank:])" >> $GITHUB_ENV
- name: Build - name: Build python-builder
uses: docker/bake-action@v5 uses: docker/build-push-action@v3
with: with:
context: .
pull: true pull: true
push: ${{ github.event_name == 'push' }} target: python-builder
files: | cache-from: type=gha,scope=python-builder
docker-bake.json cache-to: type=gha,scope=python-builder,mode=max
meta-analyzer/docker-metadata-action-bake.json
meta-api/docker-metadata-action-bake.json - name: Build python-base
meta-legacy/docker-metadata-action-bake.json uses: docker/build-push-action@v3
meta-playout/docker-metadata-action-bake.json with:
meta-worker/docker-metadata-action-bake.json context: .
set: | pull: true
*.cache-from=type=gha,scope=container target: python-base
*.cache-to=type=gha,scope=container,mode=max cache-from: type=gha,scope=python-base
*.args.LIBRETIME_VERSION=${{ env.LIBRETIME_VERSION }} cache-to: type=gha,scope=python-base,mode=max
- name: Build python-base-ffmpeg
uses: docker/build-push-action@v3
with:
context: .
pull: true
target: python-base-ffmpeg
cache-from: type=gha,scope=python-base-ffmpeg
cache-to: type=gha,scope=python-base-ffmpeg,mode=max
- name: Build analyzer
uses: ./.github/workflows/actions/build-container
with:
target: libretime-analyzer
- name: Build api
uses: ./.github/workflows/actions/build-container
with:
target: libretime-api
- name: Build playout
uses: ./.github/workflows/actions/build-container
with:
target: libretime-playout
- name: Build worker
uses: ./.github/workflows/actions/build-container
with:
target: libretime-worker
- name: Build legacy
uses: ./.github/workflows/actions/build-container
with:
target: libretime-legacy

View File

@ -15,12 +15,16 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- distribution: ubuntu
release: bionic
- distribution: ubuntu - distribution: ubuntu
release: focal release: focal
- distribution: debian
release: bullseye
- distribution: ubuntu - distribution: ubuntu
release: jammy release: jammy
- distribution: debian
release: buster
- distribution: debian
release: bullseye
- distribution: debian - distribution: debian
release: bookworm release: bookworm
@ -30,10 +34,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Login to the Container registry - name: Login to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@v2
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@ -55,7 +59,7 @@ jobs:
COPY packages.list packages.list COPY packages.list packages.list
EOF EOF
[[ "${{ matrix.release }}" == "focal" ]] && \ [[ "${{ matrix.release }}" =~ "bionic|focal" ]] && \
cat <<EOF >> Dockerfile cat <<EOF >> Dockerfile
RUN DEBIAN_FRONTEND=noninteractive apt-get --quiet update && \ RUN DEBIAN_FRONTEND=noninteractive apt-get --quiet update && \
DEBIAN_FRONTEND=noninteractive apt-get --quiet install -y software-properties-common && \ DEBIAN_FRONTEND=noninteractive apt-get --quiet install -y software-properties-common && \
@ -78,7 +82,7 @@ jobs:
EOF EOF
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: ${{ github.repository_owner == 'libretime' }} push: ${{ github.repository_owner == 'libretime' }}

View File

@ -2,21 +2,20 @@ name: Docs
on: on:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/vale/** - .github/vale/**
- .github/workflows/docs.yml - .github/workflows/docs.yml
- docs/** - docs/**
- website/**
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/vale/** - .github/vale/**
- .github/workflows/docs.yml - .github/workflows/docs.yml
- docs/** - docs/**
- website/**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs: jobs:
lint: lint:
@ -24,9 +23,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: | path: |
/usr/local/bin/vale* /usr/local/bin/vale*
@ -43,7 +42,6 @@ jobs:
errata-ai/vale \ errata-ai/vale \
vale_{version}_Linux_64-bit.tar.gz --extract vale \ vale_{version}_Linux_64-bit.tar.gz --extract vale \
/usr/local/bin/vale \ /usr/local/bin/vale \
--version v2.21.3 \
--version-file '{destination}.version' --version-file '{destination}.version'
- name: Add annotations matchers - name: Add annotations matchers
@ -53,26 +51,5 @@ jobs:
- name: Run Vale - name: Run Vale
run: | run: |
vale sync vale sync
vale --output line docs || true vale --output line docs website/src/pages || true
vale --output line --minAlertLevel=error docs/releases vale --output line --minAlertLevel=error docs/releases
sync:
name: Sync
if: >
github.repository_owner == 'libretime' &&
github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
repository: libretime/website
path: website
ssh-key: "${{ secrets.WEBSITE_DEPLOY_KEY }}"
- name: Sync docs changes
run: tools/ci-sync-docs.sh ${{ github.event.before }}..${{ github.sha }}

View File

@ -10,9 +10,6 @@ on:
required: true required: true
default: "5" default: "5"
permissions:
issues: write
jobs: jobs:
find_closed_references: find_closed_references:
if: github.repository_owner == 'libretime' if: github.repository_owner == 'libretime'
@ -20,9 +17,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: "16" node-version: "16"
@ -30,7 +27,7 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
issueLimit: ${{ github.event.inputs.issueLimit || '5' }} issueLimit: ${{ github.event.inputs.issueLimit || '5' }}
ignore: .git,/docs/releases/*,CHANGELOG.md ignore: .git,/docs/releases/*,/website/versioned*,CHANGELOG.md
find_broken_links: find_broken_links:
if: github.repository_owner == 'libretime' if: github.repository_owner == 'libretime'
@ -38,9 +35,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: .lycheecache path: .lycheecache
key: housekeeping-find-broken-links-${{ github.sha }} key: housekeeping-find-broken-links-${{ github.sha }}
@ -48,59 +45,23 @@ jobs:
- name: Check Links - name: Check Links
id: lychee id: lychee
uses: lycheeverse/lychee-action@v2.1.0 uses: lycheeverse/lychee-action@v1.5.1
with: with:
args: >- args: >-
'**/*.md' '**/*.md'
--exclude-path 'website/versioned_docs'
--require-https --require-https
--exclude-all-private --exclude-all-private
--exclude-mail
--exclude 'example\.(com|org)' --exclude 'example\.(com|org)'
--exclude '\$server_name\$request_uri' --exclude '\$server_name\$request_uri'
--exclude '%7Bvars.version%7D' --exclude '%7Bvars.version%7D'
--exclude 'https://dir.xiph.org/cgi-bin/yp-cgi' --exclude 'https://dir.xiph.org/cgi-bin/yp-cgi'
--exclude 'https://radio.indymedia.org/cgi-bin/yp-cgi' --exclude 'https://radio.indymedia.org/cgi-bin/yp-cgi'
--exclude 'https://www.ascap.com' --exclude 'https://www.ascap.com'
--exclude 'https://www.youtube-nocookie.com'
--exclude 'github\.com/libretime/libretime/(issues|pulls)' --exclude 'github\.com/libretime/libretime/(issues|pulls)'
--exclude 'https://packages.ubuntu.com/bionic/php7.2'
--exclude 'https://packages.ubuntu.com/bionic/python3'
--cache --cache
--max-cache-age 2d --max-cache-age 2d
fail: true fail: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
find_stale_issues:
if: github.repository_owner == 'libretime'
name: Find stale issues
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
activity in the last 5 months. It will be closed if no activity occurs in
the next month.
Please chat to us on the [forum](https://discourse.libretime.org/) or
ask for help on [#libretime:matrix.org](https://matrix.to/#/#libretime:matrix.org)
if you have any questions or need further support with getting this issue resolved.
You may also label an issue as *pinned* if you would like to make sure
that it does not get closed by this bot.
close-issue-message: >
This issue has been automatically closed after is was marked as stale and
did not receive any further inputs.
Feel free to let us know on the [forum](https://discourse.libretime.org/) or
ask for help on [#libretime:matrix.org](https://matrix.to/#/#libretime:matrix.org)
if you feel this issue should not have been closed.
Thank you for your contributions.
days-before-issue-stale: 150
days-before-issue-close: 30
stale-issue-label: "status: stalled"
exempt-issue-labels: "status: pinned,status: maybe later,security,is: feature-request"
exempt-issue-assignees: true
exempt-issue-milestones: true

View File

@ -1,24 +1,20 @@
name: Legacy name: Legacy
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/legacy.yml - .github/workflows/legacy.yml
- api/** - api/**
- legacy/** - legacy/**
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/legacy.yml - .github/workflows/legacy.yml
- api/** - api/**
- legacy/** - legacy/**
schedule:
- cron: 0 1 * * 1
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -30,10 +26,12 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- php-version: "7.4" # Focal, Bullseye - php-version: "7.2" # Bionic
- php-version: "7.3" # Buster
- php-version: "7.4" # Bullseye, Focal
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2 - uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php-version }} php-version: ${{ matrix.php-version }}
@ -48,12 +46,14 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- php-version: "7.4" # Focal, Bullseye - php-version: "7.2" # Bionic
- php-version: "7.3" # Buster
- php-version: "7.4" # Bullseye, Focal
env: env:
ENVIRONMENT: testing ENVIRONMENT: testing
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Setup PostgreSQL - name: Setup PostgreSQL
run: | run: |
@ -62,7 +62,6 @@ jobs:
sudo -u postgres psql -c 'CREATE DATABASE libretime;' sudo -u postgres psql -c 'CREATE DATABASE libretime;'
sudo -u postgres psql -c "CREATE USER libretime WITH PASSWORD 'libretime';" sudo -u postgres psql -c "CREATE USER libretime WITH PASSWORD 'libretime';"
sudo -u postgres psql -c 'GRANT CONNECT ON DATABASE libretime TO libretime;' sudo -u postgres psql -c 'GRANT CONNECT ON DATABASE libretime TO libretime;'
sudo -u postgres psql -c 'ALTER DATABASE libretime OWNER TO libretime;'
sudo -u postgres psql -c 'ALTER USER libretime CREATEDB;' sudo -u postgres psql -c 'ALTER USER libretime CREATEDB;'
- name: Setup PHP - name: Setup PHP
@ -73,9 +72,9 @@ jobs:
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache
run: | run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ${{ steps.composer-cache.outputs.dir }} path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@ -85,36 +84,3 @@ jobs:
- name: Run tests - name: Run tests
run: make test run: make test
working-directory: legacy working-directory: legacy
locale:
runs-on: ubuntu-latest
if: >
github.repository_owner == 'libretime' && (
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch'
)
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.LIBRETIME_BOT_TOKEN }}
- name: Install dependencies
run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gettext
- name: Update locales
run: |
git config --global user.name "libretime-bot"
git config --global user.email "libretime-bot@users.noreply.github.com"
git pull
make -C legacy/locale update
git add legacy/locale
git diff-index --quiet HEAD -- legacy/locale || {
git commit --message "chore(legacy): update locales"
git push
}

View File

@ -1,29 +1,23 @@
name: Playout name: Playout
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/playout.yml - .github/workflows/playout.yml
- playout/** - playout/**
- api-client/** - api-client/**
- shared/** - shared/**
- tools/python*
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/playout.yml - .github/workflows/playout.yml
- playout/** - playout/**
- api-client/** - api-client/**
- shared/** - shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs: jobs:
python: python:

View File

@ -12,7 +12,7 @@ jobs:
name: Validate PR title name: Validate PR title
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v5.5.3 - uses: amannn/action-semantic-pull-request@v4.6.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@ -27,8 +27,6 @@ jobs:
ui ui
worker worker
deps deps
main
stable
subjectPattern: ^(?![A-Z]).+$ subjectPattern: ^(?![A-Z]).+$
subjectPatternError: | subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}" The subject "{subject}" found in the pull request title "{title}"

View File

@ -1,12 +1,11 @@
name: Project name: Project
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
pull_request: pull_request:
types: [opened, reopened, synchronize, edited] types: [opened, reopened, synchronize, edited]
branches: [main, stable-*] branches: [main]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -15,26 +14,67 @@ jobs:
pre-commit: pre-commit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-project-pre-commit-pip-${{ hashFiles('.pre-commit-config.yaml') }} key: ${{ runner.os }}-project-pre-commit-pip-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-project-pre-commit-pip ${{ runner.os }}-project-pre-commit-pip
- uses: pre-commit/action@v3.0.1 - uses: pre-commit/action@v3.0.0
check-shell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-project-check-shell-pip
restore-keys: |
${{ runner.os }}-project-check-shell-pip
- uses: actions/cache@v3
with:
path: |
/usr/local/bin/shellcheck*
/usr/local/bin/shfmt*
key: ${{ runner.os }}-project-check-shell-tools
restore-keys: |
${{ runner.os }}-project-check-shell-tools
- run: |
python -m venv venv && source venv/bin/activate
pip install gh-release-install
sudo venv/bin/gh-release-install \
koalaman/shellcheck \
shellcheck-{tag}.linux.x86_64.tar.xz --extract shellcheck-{tag}/shellcheck \
/usr/local/bin/shellcheck \
--version-file '{destination}.version'
sudo venv/bin/gh-release-install \
mvdan/sh \
shfmt_{tag}_linux_amd64 \
/usr/local/bin/shfmt \
--version-file '{destination}.version'
- run: SEVERITY=warning make shell-check
test-tools: test-tools:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: "3.x" python-version: "3.x"
- run: make all - run: make all

View File

@ -1,21 +0,0 @@
name: Release-Please
on:
push:
branches:
- main
- stable
jobs:
release-please:
# Do not run on forks.
if: github.repository == 'libretime/libretime'
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4
with:
token: ${{ secrets.LIBRETIME_BOT_TOKEN }}
config-file: .github/release-please-config.json
manifest-file: .github/release-please-manifest.json
target-branch: ${{ github.ref_name }}

View File

@ -9,7 +9,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2 - uses: shivammathur/setup-php@v2
with: with:
php-version: 7.4 php-version: 7.4
@ -22,9 +22,10 @@ jobs:
- name: Build tarball - name: Build tarball
run: make tarball run: make tarball
- name: Upload tarball - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
with: with:
files: | body_path: docs/releases/${{ github.ref_name }}.md
libretime-*.tar.gz draft: true
sha256sums.txt prerelease: true
files: libretime-*.tar.gz

View File

@ -1,25 +1,19 @@
name: Shared name: Shared
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/shared.yml - .github/workflows/shared.yml
- shared/** - shared/**
- tools/python*
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/shared.yml - .github/workflows/shared.yml
- shared/** - shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs: jobs:
python: python:

View File

@ -0,0 +1,118 @@
name: Website Preview
on:
workflow_dispatch:
inputs:
pull-request-number:
description: "Pull request number to preview"
required: true
type: string
pull_request_target:
types: [closed]
branches: [main]
paths:
- website/**
- docs/**
env:
# PREVIEW_DEPLOY_KEY is present in the secrets
PREVIEW_EXTERNAL_REPOSITORY: libretime/libretime.github.io
PREVIEW_EXTERNAL_REPOSITORY_BRANCH: gh-pages
PREVIEW_URL: https://libretime.github.io
PREVIEW_BASE_URL: /
jobs:
deploy:
if: github.event_name == 'workflow_dispatch'
name: Deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout pull request
run: hub pr checkout ${{ github.event.inputs.pull-request-number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "16"
cache: yarn
cache-dependency-path: website/yarn.lock
- name: Install
working-directory: website
run: yarn install --frozen-lockfile
- name: Build
working-directory: website
run: yarn build
env:
URL: ${{ env.PREVIEW_URL }}
BASE_URL: ${{ env.PREVIEW_BASE_URL }}pr-${{ github.event.inputs.pull-request-number }}/
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
external_repository: ${{ env.PREVIEW_EXTERNAL_REPOSITORY }}
deploy_key: ${{ secrets.PREVIEW_DEPLOY_KEY }}
publish_dir: website/build
destination_dir: pr-${{ github.event.inputs.pull-request-number }}
full_commit_message: "deploy pr-${{ github.event.inputs.pull-request-number }}"
keep_files: true
- name: Find deployment comment
uses: peter-evans/find-comment@v2
id: find-comment
with:
issue-number: ${{ github.event.inputs.pull-request-number }}
comment-author: github-actions[bot]
body-includes: Website preview deployment
- name: Notify deployment succeeded
if: ${{ success() }}
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ github.event.inputs.pull-request-number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
**:rocket: Website preview deployment succeeded!**
Website preview: ${{ env.PREVIEW_URL }}${{ env.PREVIEW_BASE_URL }}pr-${{ github.event.inputs.pull-request-number }}/
New docs preview: ${{ env.PREVIEW_URL }}${{ env.PREVIEW_BASE_URL }}pr-${{ github.event.inputs.pull-request-number }}/docs/next/
Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Notify deployment failed
if: ${{ failure() }}
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ github.event.inputs.pull-request-number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
**:boom: Website preview deployment failed!**
Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
clean:
if: github.event_name == 'pull_request_target' && github.event.action == 'closed'
name: Clean
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
repository: ${{ env.PREVIEW_EXTERNAL_REPOSITORY }}
ref: ${{ env.PREVIEW_EXTERNAL_REPOSITORY_BRANCH }}
ssh-key: ${{ secrets.PREVIEW_DEPLOY_KEY }}
- name: Remove files
run: rm -fR pr-${{ github.event.pull_request.number }}
- uses: endbug/add-and-commit@v9
with:
message: "clean pr-${{ github.event.pull_request.number }}"

53
.github/workflows/website.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: Website
on:
push:
branches: [main]
paths:
- .github/workflows/website.yml
- docs/**
- website/**
pull_request:
branches: [main]
paths:
- .github/workflows/website.yml
- docs/**
- website/**
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "16"
cache: yarn
cache-dependency-path: ./website/yarn.lock
- uses: actions/cache@v3
with:
path: website/.docusaurus
key: docusaurus-main-${{ github.sha }}
restore-keys: |
docusaurus-main-
- name: Install
working-directory: website
run: yarn install --frozen-lockfile
- name: Build
working-directory: website
run: yarn build
- name: Deploy
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: website/build

View File

@ -1,25 +1,19 @@
name: Worker name: Worker
on: on:
workflow_dispatch:
push: push:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/worker.yml - .github/workflows/worker.yml
- worker/** - worker/**
- tools/python*
pull_request: pull_request:
branches: [main, stable-*] branches: [main]
paths: paths:
- .github/workflows/_python.yml - .github/workflows/_python.yml
- .github/workflows/worker.yml - .github/workflows/worker.yml
- worker/** - worker/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs: jobs:
python: python:

7
.gitignore vendored
View File

@ -8,13 +8,6 @@
*~ *~
VERSION VERSION
/dev/certs/*
/dev/playout/*
/website/
!.gitkeep
## Github Python .gitignore ## Github Python .gitignore
## See https://github.com/github/gitignore/blob/master/Python.gitignore ## See https://github.com/github/gitignore/blob/master/Python.gitignore
################################################################################ ################################################################################

View File

@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v4.3.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-case-conflict - id: check-case-conflict
@ -26,63 +26,48 @@ repos:
exclude: \.ambr$ exclude: \.ambr$
- id: name-tests-test - id: name-tests-test
exclude: ^api # TODO: Remove once the django api uses pytest
exclude: ^(api.*)$
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0 rev: v2.7.1
hooks: hooks:
- id: prettier - id: prettier
files: \.(md|mdx|yml|yaml|js|jsx|ts|tsx|json|css)$ files: \.(md|mdx|yml|yaml|js|jsx|ts|tsx|json|css)$
exclude: ^(legacy/public(?!/js/airtime)|CHANGELOG.md$|.github/release-please-manifest.json) exclude: ^legacy/public(?!/js/airtime)
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.19.0 rev: v2.38.2
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py3-plus, --py36-plus]
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/psf/black
rev: 1.22.2 rev: 22.8.0
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: [--resolve-all-configs]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.10.0
hooks: hooks:
- id: black - id: black
# - repo: https://github.com/pycqa/isort
# rev: 5.9.3
# hooks:
# - id: isort
# args: ["--profile", "black", "--filter-files"]
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.3.0 rev: v2.2.1
hooks: hooks:
- id: codespell - id: codespell
args: [--ignore-words=.codespellignore] args:
- --ignore-words=.codespellignore
- --builtin=clear,rare,informal
exclude: (^api/schema.yml|^legacy.*|yarn\.lock)$ exclude: (^api/schema.yml|^legacy.*|yarn\.lock)$
- repo: local - repo: local
hooks: hooks:
- id: shfmt
name: shfmt
language: docker_image
entry: mvdan/shfmt -i 2 -ci -sr -kp -w
types: [shell]
- id: shellcheck
name: shellcheck
language: docker_image
entry: koalaman/shellcheck --color=always --severity=warning
types: [shell]
- id: requirements.txt - id: requirements.txt
name: requirements.txt name: requirements.txt
description: Generate requirements.txt description: Generate requirements.txt
entry: tools/extract_requirements.py dev sentry entry: tools/extract_requirements.py dev
pass_filenames: false pass_filenames: false
language: script language: script
files: setup.py$ files: setup.py$
@ -95,14 +80,6 @@ repos:
language: script language: script
files: ^installer/config.yml$ files: ^installer/config.yml$
- id: legacy-migrations-version
name: legacy-migrations-version
description: Ensure valid schema version for migrations
entry: tools/legacy-migrations-version.sh
pass_filenames: false
language: script
files: ^api/libretime_api/legacy/migrations
- id: legacy-assets-checksum-update - id: legacy-assets-checksum-update
name: legacy-assets-checksum-update name: legacy-assets-checksum-update
description: Update legacy assets checksum description: Update legacy assets checksum
@ -110,11 +87,3 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
files: ^legacy files: ^legacy
- id: api-schema-update
name: api-schema-update
description: Ensure API schema is up to date
entry: make -C api schema
pass_filenames: false
language: system
files: ^api

View File

@ -2,16 +2,17 @@ StylesPath = .github/vale/styles
MinAlertLevel = warning MinAlertLevel = warning
Packages = \ Packages = \
https://github.com/errata-ai/Google/releases/latest/download/Google.zip, \
https://github.com/errata-ai/Microsoft/releases/latest/download/Microsoft.zip https://github.com/errata-ai/Microsoft/releases/latest/download/Microsoft.zip
Vocab = Docs Vocab = Docs
[*.md] [*.md]
BasedOnStyles = Vale, Microsoft, LibreTime BasedOnStyles = Vale, Google, Microsoft, LibreTime
# Exclude emoji shortcodes `:tada:` # Exclude emoji shortcodes `:tada:`
BlockIgnores = (:[a-z-_]+:) BlockIgnores = (:[a-z-_]+:)
Google.Units = False
Microsoft.GeneralURL = False Microsoft.GeneralURL = False
Microsoft.RangeFormat = False
Vale.Spelling = False Vale.Spelling = False

View File

@ -1,437 +1,4 @@
# Changelog <a name="3.0.0-beta.1"></a>
## [4.2.0](https://github.com/libretime/libretime/compare/4.1.0...4.2.0) (2024-06-22)
### Features
* **legacy:** add current date macro to string block criteria ([#3013](https://github.com/libretime/libretime/issues/3013)) ([451652b](https://github.com/libretime/libretime/commit/451652bc4002b142ab9cf33ae517451c4966134f))
* **legacy:** add filename block criteria ([#3015](https://github.com/libretime/libretime/issues/3015)) ([4642b6c](https://github.com/libretime/libretime/commit/4642b6c08ef813ab5dc7354f73141239f5c145e0))
### Bug Fixes
* pin pip version to &lt;24.1 to allow installing pytz (celery) ([#3043](https://github.com/libretime/libretime/issues/3043)) ([646bc81](https://github.com/libretime/libretime/commit/646bc817246a1e3e0d8107c2b69d726681c643b6))
* playlist allocates inaccurate time to smartblocks ([#3026](https://github.com/libretime/libretime/issues/3026)) ([2b43e51](https://github.com/libretime/libretime/commit/2b43e51ed140bf307e491f0fcb7b84f95709d604))
### Performance Improvements
* optimize the api image health check ([#3038](https://github.com/libretime/libretime/issues/3038)) ([d99d6e1](https://github.com/libretime/libretime/commit/d99d6e1a68f20b3f4255296cd22ac80a90adc020))
* optimize the rabbitmq health check ([#3037](https://github.com/libretime/libretime/issues/3037)) ([9684214](https://github.com/libretime/libretime/commit/96842144257855df86085b052ed8ff87562bc049))
## [4.1.0](https://github.com/libretime/libretime/compare/4.0.0...4.1.0) (2024-05-05)
### Features
* **api:** implement file deletion ([#2960](https://github.com/libretime/libretime/issues/2960)) ([9757b1b](https://github.com/libretime/libretime/commit/9757b1b78c98a33f233163c77eb1b2ad6e0f0efe))
* build schedule events exclusively in playout ([#2946](https://github.com/libretime/libretime/issues/2946)) ([40b4fc7](https://github.com/libretime/libretime/commit/40b4fc7f66004ee3bcb61c9961ec2c48bbcbc6cb))
* **legacy:** add aac/opus support to dashboard player ([#2881](https://github.com/libretime/libretime/issues/2881)) ([95283ef](https://github.com/libretime/libretime/commit/95283efc1f9a63376a99184ef69b699beba45802))
* **legacy:** disable public radio page and redirect to login ([#2903](https://github.com/libretime/libretime/issues/2903)) ([170d095](https://github.com/libretime/libretime/commit/170d09545e4fcfeeb95f9fc5c355329764501854))
* **legacy:** trim overbooked shows after autoloading a playlist ([#2897](https://github.com/libretime/libretime/issues/2897)) ([a95ce3d](https://github.com/libretime/libretime/commit/a95ce3d2296bb864b379dcce14090bd821c1dfc9))
* **legacy:** visual cue point editor ([#2947](https://github.com/libretime/libretime/issues/2947)) ([da02e74](https://github.com/libretime/libretime/commit/da02e74f2115cb76a6435fab5ab2667a8c622b98))
* start celery worker programmatically ([#2988](https://github.com/libretime/libretime/issues/2988)) ([9c548b3](https://github.com/libretime/libretime/commit/9c548b365ec114c6789d2a69e66cc721da6ae100))
### Bug Fixes
* **analyzer:** backslash non utf-8 data when probing replaygain ([#2931](https://github.com/libretime/libretime/issues/2931)) ([29f73e0](https://github.com/libretime/libretime/commit/29f73e0dcb1fd668a79a2ffedc33e16172277376)), closes [#2910](https://github.com/libretime/libretime/issues/2910)
* apply replay gain preferences on scheduled files ([#2945](https://github.com/libretime/libretime/issues/2945)) ([35d0dec](https://github.com/libretime/libretime/commit/35d0dec4a887cdaea2d73dc9bee60eb6624a2aca))
* **deps:** update dependency friendsofphp/php-cs-fixer to &lt;3.49.1 ([#2899](https://github.com/libretime/libretime/issues/2899)) ([3e05748](https://github.com/libretime/libretime/commit/3e05748d2d1180b8dad55b6f997e6aa7117735f1))
* **deps:** update dependency friendsofphp/php-cs-fixer to &lt;3.51.1 ([#2963](https://github.com/libretime/libretime/issues/2963)) ([22c303c](https://github.com/libretime/libretime/commit/22c303cfffdc777177bd74273e2c24da58cf1682))
* **deps:** update dependency friendsofphp/php-cs-fixer to &lt;3.53.1 ([#2972](https://github.com/libretime/libretime/issues/2972)) ([9192aaa](https://github.com/libretime/libretime/commit/9192aaa2bb2dada470e03537493160d9b14a42f4))
* **deps:** update dependency gunicorn to v22 (security) ([#2993](https://github.com/libretime/libretime/issues/2993)) ([a2cf769](https://github.com/libretime/libretime/commit/a2cf7697a97bbc4faf89fd7bc9ba9ecc235bf873))
* incorrect docker compose version ([#2975](https://github.com/libretime/libretime/issues/2975)) ([634e6e2](https://github.com/libretime/libretime/commit/634e6e236d908994d586c946bbe28bcba8a357fa))
* **installer:** setup the worker entrypoint ([#2996](https://github.com/libretime/libretime/issues/2996)) ([71b20ae](https://github.com/libretime/libretime/commit/71b20ae3c974680d814062c5a0bfa51a105dde61))
* **legacy:** allow deleting file with api token ([#2995](https://github.com/libretime/libretime/issues/2995)) ([86da46e](https://github.com/libretime/libretime/commit/86da46ee3a54676298e30301846be890d1ea93ae))
* **legacy:** allow updating track types code ([#2955](https://github.com/libretime/libretime/issues/2955)) ([270aa08](https://github.com/libretime/libretime/commit/270aa08ae6c7207de1cc3ea552dabeb018bcfe0d))
* **legacy:** avoid crash when lot of streams in configuration ([#2915](https://github.com/libretime/libretime/issues/2915)) ([12dd477](https://github.com/libretime/libretime/commit/12dd47731290bf539be7a2a81571f8ada223e9c4))
* **legacy:** ensure validation is performed on the track type form ([#2985](https://github.com/libretime/libretime/issues/2985)) ([5ad69bf](https://github.com/libretime/libretime/commit/5ad69bf0b76ff2e5065551b6a7d154cb26834605))
* **legacy:** fix hidden fields in edit file form ([#2932](https://github.com/libretime/libretime/issues/2932)) ([f4b260f](https://github.com/libretime/libretime/commit/f4b260fdf70c0dd1830166d3856239dae5366599))
* **legacy:** replay_gain_modifier should be a system preference ([#2943](https://github.com/libretime/libretime/issues/2943)) ([37d1a76](https://github.com/libretime/libretime/commit/37d1a7685e37e45734553a0eb4a4da793ca858cb))
* remove obsolete docker compose version ([#2982](https://github.com/libretime/libretime/issues/2982)) ([fb0584b](https://github.com/libretime/libretime/commit/fb0584b021fd1c966181c7ab3989938cdfe4e642))
* trigger legacy tasks manager every 5m ([#2987](https://github.com/libretime/libretime/issues/2987)) ([7040d0e](https://github.com/libretime/libretime/commit/7040d0e4bd92911a9072226f49ad59ce575d6ed9))
* **worker:** ensure celery beat is started ([#3007](https://github.com/libretime/libretime/issues/3007)) ([bfde17e](https://github.com/libretime/libretime/commit/bfde17edf7fcc2bfd55263756e6ec3e455f11740))
## [4.0.0](https://github.com/libretime/libretime/compare/3.2.0...4.0.0) (2024-01-07)
### ⚠ BREAKING CHANGES
* The media file serving is now handled by Nginx instead of the API service. The `storage.path` field is now used in the Nginx configuration, so make sure to update the Nginx configuration file if you change it.
* **installer:** The default listen port for the installer is now `8080`. We recommend that you put a reverse proxy in front of LibreTime.
* **installer:** The `--update-nginx` flag was removed from the installer. The nginx configuration deployed by the installer will now always be overwritten. Make sure to move your customizations to a reverse proxy configuration.
* The default system output (`stream.outputs.system[].kind`) changed from `alsa` to `pulseaudio`. Make sure to update your configuration file if you rely on the default system output.
* The `general.secret_key` configuration field is now required. Make sure to update your configuration file and add a secret key.
### Features
* default system output is now `pulseaudio` ([#2842](https://github.com/libretime/libretime/issues/2842)) ([083ee3f](https://github.com/libretime/libretime/commit/083ee3f1dd74441e288b4d63178ae9cea12ba286)), closes [#2542](https://github.com/libretime/libretime/issues/2542)
* disable uvicorn worker lifespan ([#2845](https://github.com/libretime/libretime/issues/2845)) ([8743c84](https://github.com/libretime/libretime/commit/8743c84d0f007a5430e9059c197a261e613cc642))
* **installer:** add the `--storage-path` flag ([#2865](https://github.com/libretime/libretime/issues/2865)) ([5b23852](https://github.com/libretime/libretime/commit/5b23852f8d144f0c7cdeb62831f7b1a27872b40e))
* **installer:** change default listen port to 8080 ([#2852](https://github.com/libretime/libretime/issues/2852)) ([f72b7f9](https://github.com/libretime/libretime/commit/f72b7f9c9727800a9d77d64c540c12f272bb0ae3))
* **installer:** remove the `--update-nginx` flag ([#2851](https://github.com/libretime/libretime/issues/2851)) ([35d7eac](https://github.com/libretime/libretime/commit/35d7eace13c2b9667fdb41fec0788118e0c5e63f))
* **playout:** configure device for alsa and pulseaudio system outputs ([#2654](https://github.com/libretime/libretime/issues/2654)) ([06af18b](https://github.com/libretime/libretime/commit/06af18b84e7dfaad95e3b55dda22ec1ddad27050))
* rewrite cloud-init config ([#2853](https://github.com/libretime/libretime/issues/2853)) ([8406d52](https://github.com/libretime/libretime/commit/8406d520d7a7bea4060be8a00e360bcf413cb2d5))
* run python in optimized mode ([#2874](https://github.com/libretime/libretime/issues/2874)) ([3f7fc99](https://github.com/libretime/libretime/commit/3f7fc99b6b343fbc8df319d8130ba8247aea96d8))
* the `general.secret_key` configuration field is now required ([#2841](https://github.com/libretime/libretime/issues/2841)) ([0d2d1a2](https://github.com/libretime/libretime/commit/0d2d1a26731a2b41ce5e574ed6de9950eaae4153)), closes [#2426](https://github.com/libretime/libretime/issues/2426)
* use nginx to serve media files ([#2860](https://github.com/libretime/libretime/issues/2860)) ([4603c17](https://github.com/libretime/libretime/commit/4603c1759f29b8a1adb3e83d610ca00e778d76bd))
### Bug Fixes
* add parent function name in setValue exception ([#2777](https://github.com/libretime/libretime/issues/2777)) ([c764a5a](https://github.com/libretime/libretime/commit/c764a5a648ac6cf6c1f63cd9be6de9fe07c40988))
* **api:** ensure non ascii paths are handled by X-Accel-Redirect ([#2861](https://github.com/libretime/libretime/issues/2861)) ([0ce63f3](https://github.com/libretime/libretime/commit/0ce63f3bf0448580170024cdde96ee351ee5c358))
* **api:** enum schema description ([#2803](https://github.com/libretime/libretime/issues/2803)) ([976b70e](https://github.com/libretime/libretime/commit/976b70ed32a0e774cc0b72b8332372be32799ed1))
* **api:** let nginx handle the media file content type ([#2862](https://github.com/libretime/libretime/issues/2862)) ([72268ad](https://github.com/libretime/libretime/commit/72268ad9bb1a96b24efda7143b9371d6fd98ca03))
* **api:** move gunicorn worker config to python file ([#2854](https://github.com/libretime/libretime/issues/2854)) ([43221d9](https://github.com/libretime/libretime/commit/43221d9d7f34ba98a14db9906e350cb494a86b25))
* **api:** paths with question marks chars are handled by X-Accel-Redirect ([#2875](https://github.com/libretime/libretime/issues/2875)) ([b2c1ceb](https://github.com/libretime/libretime/commit/b2c1ceb89fafc76f18ec650d19ec0ff03e4a20b0))
* **deps:** update dependency friendsofphp/php-cs-fixer to &lt;3.42.1 (main) ([#2765](https://github.com/libretime/libretime/issues/2765)) ([8ae4dce](https://github.com/libretime/libretime/commit/8ae4dce9e7c013c1f66f1b4d5da4a8c91d3419b7))
* **deps:** update dependency friendsofphp/php-cs-fixer to &lt;3.43.2 (main) ([#2848](https://github.com/libretime/libretime/issues/2848)) ([62e5f4d](https://github.com/libretime/libretime/commit/62e5f4dfbb76ab1919c4905570cc34274c685cef))
* **deps:** update dependency friendsofphp/php-cs-fixer to &lt;3.45.1 (main) ([#2855](https://github.com/libretime/libretime/issues/2855)) ([6f84328](https://github.com/libretime/libretime/commit/6f8432838058be6ef1cfa7858f17b8272929896e))
* **deps:** update dependency friendsofphp/php-cs-fixer to &lt;3.46.1 (main) ([#2868](https://github.com/libretime/libretime/issues/2868)) ([4827dbc](https://github.com/libretime/libretime/commit/4827dbce711262e90238bb3b6c0a35b1ce3d6877))
* **legacy:** allow uploading opus files ([#2804](https://github.com/libretime/libretime/issues/2804)) ([f252a16](https://github.com/libretime/libretime/commit/f252a16637e113ceb1dd340fb7aad31af9c23ff0))
* **legacy:** declare previously undeclared variable ([#2793](https://github.com/libretime/libretime/issues/2793)) ([e2cfbf4](https://github.com/libretime/libretime/commit/e2cfbf4c038f28874a206df5805f04f69a40647b))
* **legacy:** ensure last played criteria works with never played files ([#2840](https://github.com/libretime/libretime/issues/2840)) ([24ee383](https://github.com/libretime/libretime/commit/24ee3830c23f7147f82febe3d3c6743d5ae8d4e6))
* **playout:** increase file download chunk size to 8192 bytes ([#2863](https://github.com/libretime/libretime/issues/2863)) ([7ed1be1](https://github.com/libretime/libretime/commit/7ed1be1816abef20b9ae59a8c66a9e48a34f37c5))
* **playout:** remove empty file when the download request failed ([#2864](https://github.com/libretime/libretime/issues/2864)) ([2facbfa](https://github.com/libretime/libretime/commit/2facbfaff23d4df0e7531b82f04f932bb2c4c9a4))
* **worker:** unbound variable when episode url returns HTTP 404 ([#2844](https://github.com/libretime/libretime/issues/2844)) ([3f39689](https://github.com/libretime/libretime/commit/3f396895e588e62183e01d17927d9bdbea512ee0))
## [3.2.0](https://github.com/libretime/libretime/compare/3.1.0...3.2.0) (2023-10-16)
- [Release note](https://libretime.org/docs/releases/3.2.0/)
### Features
- **legacy:** move session store to database ([#2523](https://github.com/libretime/libretime/issues/2523))
- **api:** add email configuration
- add mobile devices stream config field ([#2744](https://github.com/libretime/libretime/issues/2744))
### Bug Fixes
- **playout:** liquidsoap aac output syntax errors
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.20.1 (stable) ([#2602](https://github.com/libretime/libretime/issues/2602))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.21.2 ([#2612](https://github.com/libretime/libretime/issues/2612))
- libretime process leaks and lsof high cpu usage ([#2615](https://github.com/libretime/libretime/issues/2615))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.22.1 ([#2633](https://github.com/libretime/libretime/issues/2633))
- libretime process leaks and lsof high cpu usage ([#2615](https://github.com/libretime/libretime/issues/2615))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.23.1 (stable) ([#2656](https://github.com/libretime/libretime/issues/2656))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.26.1 (stable) ([#2678](https://github.com/libretime/libretime/issues/2678))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.26.1 (main) ([#2677](https://github.com/libretime/libretime/issues/2677))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.26.2 ([#2686](https://github.com/libretime/libretime/issues/2686))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.26.2 ([#2687](https://github.com/libretime/libretime/issues/2687))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.27.1 (main) ([#2714](https://github.com/libretime/libretime/issues/2714))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.27.1 (stable) ([#2715](https://github.com/libretime/libretime/issues/2715))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.34.1 ([#2723](https://github.com/libretime/libretime/issues/2723))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.35.2 ([#2738](https://github.com/libretime/libretime/issues/2738))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.35.2 ([#2722](https://github.com/libretime/libretime/issues/2722))
### Documentation
- update chat links to point to matrix ([#2571](https://github.com/libretime/libretime/issues/2571))
- fix broken link ([#2616](https://github.com/libretime/libretime/issues/2616))
### Tests
- **playout:** check unsupported liquidsoap aac output
## [3.1.0](https://github.com/libretime/libretime/compare/3.0.2...3.1.0) (2023-05-26)
- [Release note](https://libretime.org/docs/releases/3.1.0/)
### Features
- drop Ubuntu Bionic support
- drop Python 3.6 support
- drop Debian Buster support
- drop Liquidsoap 1.1 support
- drop Liquidsoap 1.3 support
- drop Python 3.7 support
- drop cc_stream_setting table
- delete cc_pref stream preferences rows
- **legacy:** remove db allowed_cors_origins preference ([#2095](https://github.com/libretime/libretime/issues/2095))
- configure cue points analysis per track type
- **playout:** use jinja2 env for template loading
- **playout:** add jinja2 quote filter for liquidsoap
- **playout:** use liquidsoap interactive variables
- **playout:** remove unused liquidsoap outputs connection status
- **playout:** remove unused liquidsoap restart function
- **playout:** remove unused liquidsoap output namespace
- replace loguru with logging
- **playout:** use jinja to configure liquidsoap outputs
- **playout:** enable vorbis metadata per icecast output
- **playout:** use shared app for cli commands
- **installer:** configure timezone using timedatectl ([#2418](https://github.com/libretime/libretime/issues/2418))
- **playout:** don't serialize message twice
- add python packages version
- add sentry sdk
- use secret_key config field instead of api_key ([#2444](https://github.com/libretime/libretime/issues/2444))
- **api-client:** remove unused api v1 calls
- **api-client:** rewrite api-client v1 using abstract client
- **playout:** move liquidsoap auth to notify cli
- **playout:** replace schedule event dicts with objects
- **api:** add cors headers middleware ([#2479](https://github.com/libretime/libretime/issues/2479))
- **playout:** replace thread timeout with socket timeout
- remove dev files from tarball
- include tarball checksums in releases
- set icecast mount default charset to UTF-8
- **playout:** allow harbor ssl configuration
- **api:** install gunicorn/uvicorn from pip
- install inside a python3 venv
### Bug Fixes
- **deps:** update dependency adbario/php-dot-notation to v3 ([#2226](https://github.com/libretime/libretime/issues/2226))
- **deps:** update dependency league/uri to v6.7.2
- **legacy:** set platform requirements to php ^7.4
- **playout:** remove outdated liquidsoap code
- **playout:** add types
- **api:** allow single digit version for legacy schema
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.12.1
- remove systemd ProtectHome feature ([#2243](https://github.com/libretime/libretime/issues/2243))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.13.1 ([#2249](https://github.com/libretime/libretime/issues/2249))
- **worker:** replace deprecated cgi.parse_header
- **installer:** install missing sudo
- **installer:** set home and login when running as postgres
- **legacy:** add log entry on task run ([#2316](https://github.com/libretime/libretime/issues/2316))
- **legacy:** log errors on connect check failure ([#2317](https://github.com/libretime/libretime/issues/2317))
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.13.2
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.13.3
- **legacy:** advanced search by track type id
- **legacy:** move forked deps to the libretime namespace
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.14.4
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.14.5
- **legacy:** ensure options is a dict during json encoding
- **legacy:** don't use dict assignment on object ([#2384](https://github.com/libretime/libretime/issues/2384))
- **playout:** quote escape strings in liquidsoap entrypoint
- **legacy:** do not delete audio file when removing artwork ([#2395](https://github.com/libretime/libretime/issues/2395))
- **playout:** use explicit ids for liquidsoap components
- **playout:** skip the identified queue instead of the current
- **playout:** use the same number of schedule queues
- **legacy:** on air light fails when no shows are scheduled
- **playout:** flush liquidsoap response before sending new
- **playout:** use package loader for liquidsoap templates
- **playout:** %else is not defined
- **playout:** when shows ends, next shows starts without fade-in/fade-out ([#2412](https://github.com/libretime/libretime/issues/2412))
- **playout:** legacy pushes non validated data
- **playout:** explicit ogg vorbis icecast encoder
- **playout:** prevent unbound variables
- **playout:** use int for liquidsoap queues map
- **shared:** return type confusion
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.15.2
- **api:** explicit FileImportStatusEnum in schema
- pin postgresql version in docker-compose
- pin rabbitmq version in docker-compose
- allow overriding docker-compose predefined environment
- move docker specific setup to dockerfile
- **api:** cast string value to int enum ([#2461](https://github.com/libretime/libretime/issues/2461))
- **playout:** quote incompatible <py3.9 type hints
- **installer:** bump setuptools to ~=67.3 ([#2387](https://github.com/libretime/libretime/issues/2387))
- **playout:** use new api-client v1
- **playout:** catch oserror in liquidsoap client
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.16.1 (main) ([#2490](https://github.com/libretime/libretime/issues/2490))
- **api:** require django >=4.2.0,<4.3
- **api:** upgrade psycopg to v3.1
- **playout:** remove unused ecasound package ([#2496](https://github.com/libretime/libretime/issues/2496))
- **installer:** ignore whitespace during diff
- **legacy:** don't print track_type id in show builder table ([#2510](https://github.com/libretime/libretime/issues/2510))
- **legacy:** remove composer superuser warning ([#2515](https://github.com/libretime/libretime/issues/2515))
- **legacy:** keep datatable settings between views ([#2519](https://github.com/libretime/libretime/issues/2519))
- **api:** upgrade django code (pre-commit)
- **analyzer:** remove unused python3 package
- **deps:** update dependency friendsofphp/php-cs-fixer to <3.17.1 (main) ([#2556](https://github.com/libretime/libretime/issues/2556))
### Documentation
- **playout:** add simple inputs pipeline schema ([#2240](https://github.com/libretime/libretime/issues/2240))
- add DOCKER_BUILDKIT env variable for docker-compose v1 ([#2270](https://github.com/libretime/libretime/issues/2270))
- no need to update release note path
- adapt c4 to our workflows
- stop providing maintenance releases for old distributions
- add pulseaudio output in containers tutorial ([#2166](https://github.com/libretime/libretime/issues/2166))
- remove warning about docker install ([#2411](https://github.com/libretime/libretime/issues/2411))
- docker-compose env variables setup
- add instructions for the sentry setup ([#2441](https://github.com/libretime/libretime/issues/2441))
- upgrade by migrating to a new server
- fix database backup and restore commands
- move contributing to docs/contribute
- split developer and contributor manual
- extract dev workflows from contributing docs
- add some history notes
- move release docs in the release section
- fix broken links
- ignore range format during docs linting
- only use microsoft styling guide
- move configuration documentation
- rename setup to install
- split install guide per install method
- docker config template install with envsubst ([#2517](https://github.com/libretime/libretime/issues/2517))
- improve reverse proxy docs
- improve install guides
- add certbot setup guide
- ensure example values are replaced
- fix broken link ([#2532](https://github.com/libretime/libretime/issues/2532))
- add note about unused packages
- improve airtime migration guide ([#2564](https://github.com/libretime/libretime/issues/2564))
- split airtime migration into more steps ([#2565](https://github.com/libretime/libretime/issues/2565))
- remove setup without reverse proxy
- fix icecast certificates bundle command
- install using a reverse proxy by default
- be consistent with example domain ([#2568](https://github.com/libretime/libretime/issues/2568))
- add 3.1.x distribution releases support
### Tests
- liquidsoap package from ppa is version 1.4.2 ([#2223](https://github.com/libretime/libretime/issues/2223))
- **playout:** refresh snapshots after major upgrade
- re-enable pylint logging-fstring-interpolation
- **playout:** more entrypoint config test cases
- **playout:** generated liquidsoap script syntax
- **playout:** silence existing broad-exception-caught errors
- **playout:** allow pylint failure
- **playout:** check untyped defs with mypy
- **api:** fix linting errors
- **shared:** fix linting errors
- **api:** fix linting errors
- **shared:** fix linting errors
- **playout:** class creation
- **api-client:** allow linters failure
- **playout:** move liq_conn fixture to conftest
- **playout:** liquidsoap wait for version
- **api:** add django-upgrade pre-commit hook
### CI
- test project weekly
- enable renovate for 3.0.x ([#2277](https://github.com/libretime/libretime/issues/2277))
- sync docs with libretime/website repository
- pin vale version to v2.21.3
- don't squash commits during docs sync
- always print diff when schema changes
- check if locale are up to date
- update locales weekly, not for every commit ([#2403](https://github.com/libretime/libretime/issues/2403))
- use bake file for container build
- allow manual ci trigger
- use bot to update locales
- replace deprecated set-output ([#2408](https://github.com/libretime/libretime/issues/2408))
- update docker hub containers description
- replace stale bot with stale action ([#2421](https://github.com/libretime/libretime/issues/2421))
- allow Falso as a word in codespell
- allow Falso as a word in codespell
- run all tests on python tools changes
- don't run stale bot on feature requests ([#2527](https://github.com/libretime/libretime/issues/2527))
### Reverts
- chore(api): install django-rest-framework from git ([#2518](https://github.com/libretime/libretime/issues/2518))
## [3.0.2](https://github.com/libretime/libretime/compare/3.0.1...3.0.2) (2023-02-21)
- [Release note](https://libretime.org/docs/releases/3.0.2/)
### Bug Fixes
- **legacy:** advanced search by track type id
- **legacy:** refresh lock files
- **legacy:** move forked deps to the libretime namespace
- **legacy:** improve error messages and logs
- **installer:** allow different actions on template_file
- **installer:** print diff on file deployment
- **installer:** only setup nginx on first install
- **installer:** print unsupported distribution error ([#2368](https://github.com/libretime/libretime/issues/2368))
- **installer:** create systemd dirs if missing ([#2379](https://github.com/libretime/libretime/issues/2379))
### Documentation
- add DOCKER_BUILDKIT env variable for docker-compose v1 ([#2270](https://github.com/libretime/libretime/issues/2270))
- check logs before checking services status
- add small faq for troubleshooting
### Tests
- **playout:** refresh snapshots after major upgrade ([#2381](https://github.com/libretime/libretime/issues/2381))
### CI
- don't squash commits during docs sync
- test project weekly
## [3.0.1](https://github.com/libretime/libretime/compare/3.0.0...3.0.1) (2022-12-20)
- [Release note](https://libretime.org/docs/releases/3.0.1/)
### Bug Fixes
- remove systemd ProtectHome feature ([#2244](https://github.com/libretime/libretime/issues/2244))
- **installer:** install missing sudo
- **installer:** set home and login when running as postgres
- **legacy:** add log entry on task run ([#2316](https://github.com/libretime/libretime/issues/2316))
- **legacy:** log errors on connect check failure ([#2317](https://github.com/libretime/libretime/issues/2317))
- **worker:** replace deprecated cgi.parse_header
### Documentation
- no need to update release note path
### Tests
- liquidsoap package from ppa is version 1.4.2 ([#2233](https://github.com/libretime/libretime/issues/2233))
### CI
- run tests on 3.0.x
- enable renovate bot on 3.0.x
- sync docs with libretime/website repository
- pin vale version to v2.21.3
## [3.0.0](https://github.com/libretime/libretime/compare/3.0.0-beta.2...3.0.0) (2022-10-10)
- [Release note](https://libretime.org/docs/releases/3.0.0/)
### Bug Fixes
- clean exit by catching keyboard interrupt ([#2206](https://github.com/libretime/libretime/issues/2206))
- **legacy:** missing plupload uk_UA translation
- **legacy:** jquery i18n translations for plupload
- **legacy:** gracefully handle missing asset checksum
- disable some systemd security features on bionic ([#2219](https://github.com/libretime/libretime/issues/2219))
### Documentation
- **legacy:** how to add a new language
### Tests
- **analyzer:** fix wrong bit_rate values
## [3.0.0-beta.2](https://github.com/libretime/libretime/compare/3.0.0-beta.1...3.0.0-beta.2) (2022-10-03)
- [Release note](https://libretime.org/docs/releases/3.0.0-beta.2/)
### Features
- systemd service hardening ([#2186](https://github.com/libretime/libretime/issues/2186))
- extra systemd service hardening ([#2197](https://github.com/libretime/libretime/issues/2197))
### Bug Fixes
- start playout service after liquidsoap ([#2164](https://github.com/libretime/libretime/issues/2164))
- include version variable inside containers
- change version format
- **legacy:** add play button to stream player ([#2190](https://github.com/libretime/libretime/issues/2190))
- **legacy:** correct log levels ([#2196](https://github.com/libretime/libretime/issues/2196))
### Documentation
- remove breaking change warning ([#2180](https://github.com/libretime/libretime/issues/2180))
- fix vale linting errors
- fix vale linting error
### CI
- allow failure when linting /docs/releases
- use github.ref_name to get tag
## [3.0.0-beta.1](https://github.com/libretime/libretime/compare/3.0.0-beta.0...3.0.0-beta.1) (2022-09-23) ## [3.0.0-beta.1](https://github.com/libretime/libretime/compare/3.0.0-beta.0...3.0.0-beta.1) (2022-09-23)
@ -465,6 +32,8 @@
- don't check github.com/libretime/libretime/(issues|pulls) links - don't check github.com/libretime/libretime/(issues|pulls) links
- run docs workflow on vale files changes - run docs workflow on vale files changes
<a name="3.0.0-beta.0"></a>
## [3.0.0-beta.0](https://github.com/libretime/libretime/compare/3.0.0-alpha.13...3.0.0-beta.0) (2022-09-16) ## [3.0.0-beta.0](https://github.com/libretime/libretime/compare/3.0.0-alpha.13...3.0.0-beta.0) (2022-09-16)
- [Release note](https://libretime.org/docs/releases/3.0.0-beta.0/) - [Release note](https://libretime.org/docs/releases/3.0.0-beta.0/)
@ -614,6 +183,8 @@
- improve containers build caching - improve containers build caching
- add container tags - add container tags
<a name="3.0.0-alpha.13"></a>
## [3.0.0-alpha.13](https://github.com/libretime/libretime/compare/3.0.0-alpha.12...3.0.0-alpha.13) (2022-07-15) ## [3.0.0-alpha.13](https://github.com/libretime/libretime/compare/3.0.0-alpha.12...3.0.0-alpha.13) (2022-07-15)
- [Release note](https://libretime.org/docs/releases/3.0.0-alpha.13/) - [Release note](https://libretime.org/docs/releases/3.0.0-alpha.13/)
@ -774,6 +345,8 @@
- disable codecov project status check - disable codecov project status check
- disable codecov patch status check - disable codecov patch status check
<a name="3.0.0-alpha.12"></a>
## [3.0.0-alpha.12](https://github.com/libretime/libretime/compare/3.0.0-alpha.11...3.0.0-alpha.12) (2022-03-29) ## [3.0.0-alpha.12](https://github.com/libretime/libretime/compare/3.0.0-alpha.11...3.0.0-alpha.12) (2022-03-29)
- [Release note](https://libretime.org/docs/releases/3.0.0-alpha.12/) - [Release note](https://libretime.org/docs/releases/3.0.0-alpha.12/)
@ -788,6 +361,8 @@
- add missing data to release note - add missing data to release note
- fix and update links ([#1714](https://github.com/libretime/libretime/issues/1714)) - fix and update links ([#1714](https://github.com/libretime/libretime/issues/1714))
<a name="3.0.0-alpha.11"></a>
## [3.0.0-alpha.11](https://github.com/libretime/libretime/compare/3.0.0-alpha.10...3.0.0-alpha.11) (2022-03-28) ## [3.0.0-alpha.11](https://github.com/libretime/libretime/compare/3.0.0-alpha.10...3.0.0-alpha.11) (2022-03-28)
- [Release note](https://libretime.org/docs/releases/3.0.0-alpha.11/) - [Release note](https://libretime.org/docs/releases/3.0.0-alpha.11/)

View File

@ -1 +0,0 @@
docs/contribute.md

129
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,129 @@
# Contributing to LibreTime
First and foremost, thank you! We appreciate that you want to contribute to
LibreTime, your time is valuable, and your contributions mean a lot to us.
Before any contribution, read and be prepared to adhere to our
[code of conduct](https://github.com/libretime/organization/blob/main/CODE_OF_CONDUCT.md).
In addition, LibreTime follow the standardized
[C4 development process](https://rfc.zeromq.org/spec:42/c4/), in which you can
find explanation about most of the development workflows for LibreTime.
**How to contribute**
- [Reporting bugs](#reporting-bugs)
- [Suggesting enhancements](#suggesting-enhancements)
- [Financially](https://libretime.org/contribute#financial)
- [Contributing to documentation](https://libretime.org/contribute#write-documentation)
- [Contributing to code](#code)
## Reporting bugs
This section guides you through submitting a bug report for LibreTime.
Following these guidelines helps maintainers and the community understand your
report, reproduce the behavior, and find related reports.
Before creating bug reports, please check the following list, to be sure that
you need to create one:
- **Check the [LibreTime forum](https://discourse.libretime.org/)** for existing
questions and discussion.
- **Check that your issue does not already exist in the
[issue tracker](https://github.com/libretime/libretime/issues?q=is%3aissue+label%3abug)**.
> **Note:** If you find a **Closed** issue that seems like it is the same thing
> that you're experiencing, open a new issue and include a link to the original
> issue in the body of your new one.
When you are creating a bug report, please include as many details as possible.
Fill out the [required template](https://github.com/libretime/libretime/issues/new?labels=bug&template=bug_report.md),
the information it asks helps the maintainers resolve the issue faster.
Bugs are tracked on the [official issue tracker](https://github.com/libretime/libretime/issues).
## Suggesting enhancements
This section guides you through submitting an enhancement suggestion for
LibreTime, including completely new features and minor improvements to existing
functionality. Following these guidelines helps maintainers and the community
understand your suggestion and find related suggestions.
Before creating enhancement suggestions, please check the following list, as you
might find out that you don't need to create one:
- **Check the [LibreTime forum](https://discourse.libretime.org/)** for existing
questions and discussion.
- **Check that your issue does not already exist in the
[issue tracker](https://github.com/libretime/libretime/issues?q=is%3aissue+label%3afeature-request)**.
When you are creating an enhancement suggestion, please include as many details
as possible. Fill in [the template](https://github.com/libretime/libretime/issues/new?labels=feature-request&template=feature_request.md),
including the steps that you imagine you would take if the feature you're
requesting existed.
## Code
Are you familiar with coding in PHP or Python? Have you made projects in
Liquidsoap and some of the other services we use? Take a look at the
[list of bugs and feature requests](https://github.com/libretime/libretime/issues),
and then fork our repo and have a go! Just use the **Fork** button at the top of
our [GitHub page](https://github.com/libretime/libretime), clone the forked repo
to your desktop, open up a favorite editor and make some changes, and then
commit, push, and open a pull request.
Knowledge on how to use [Github](https://guides.github.com/activities/hello-world/)
and [Git](https://git-scm.com/docs/gittutorial) will suit you well, use the
links for a quick 101.
LibreTime uses the [black](https://github.com/psf/black) coding style for Python
and you must ensure that your code follows it. If not, the CI will fail and your
Pull Request will not be merged. Similarly, the Python import statements are
sorted with [isort](https://github.com/pycqa/isort). There is configuration
provided for [pre-commit](https://pre-commit.com/), which will ensure that code
matches the expected style and conventions when you commit changes. It is set up
by running:
```bash
sudo apt install pre-commit
pre-commit install
```
You can also run it anytime using:
```bash
pre-commit run --all-files
```
## Testing and CI/CD
Before submitting code to the project, it's a good idea to test it first. To do
this, it's easiest to install LibreTime in a virtual machine on your local
system or in a cloud VM. We have instructions for setting up a virtual instance
of LibreTime with [Vagrant](/docs/vagrant) and [Multipass](/docs/multipass).
If you would like to try LibreTime in a Docker image, Odclive has instructions
[here](https://github.com/kessibi/libretime-docker) for setting up a test image
and a more persistent install.
## Modifying the Database
LibreTime is designed to work with a [PostgreSQL](https://www.postgresql.org/)
database server running locally. LibreTime uses [PropelORM](https://github.com/propelorm/Propel)
to interact with the ZendPHP components and create the database. The version 2
API uses Django to interact with the same database.
If you are a developer seeking to add new columns to the database here are the steps.
1. Modify `legacy/build/schema.xml` with any changes.
2. Run `dev_tools/propel_generate.sh`
3. Update the upgrade.sql under `legacy/application/controllers/upgrade_sql/VERSION` for example
`ALTER TABLE imported_podcast ADD COLUMN album_override boolean default 'f' NOT NULL;`
4. Update the models under `api/libretime_api/models/` to reflect the new
changes.
## Documentation and financial contributions
More information about how to contribute documentation or financially
through our [OpenCollective](https://opencollective.com/libretime) can be found
on our [website](https://libretime.org/contribute).

View File

@ -2,7 +2,7 @@ ARG LIBRETIME_VERSION
#======================================================================================# #======================================================================================#
# Python Builder # # Python Builder #
#======================================================================================# #======================================================================================#
FROM python:3.10-slim-bullseye AS python-builder FROM python:3.10-slim-bullseye as python-builder
WORKDIR /build WORKDIR /build
@ -18,7 +18,7 @@ RUN pip wheel --wheel-dir . --no-deps .
#======================================================================================# #======================================================================================#
# Python base # # Python base #
#======================================================================================# #======================================================================================#
FROM python:3.10-slim-bullseye AS python-base FROM python:3.10-slim-bullseye as python-base
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -28,9 +28,11 @@ ARG USER=libretime
ARG UID=1000 ARG UID=1000
ARG GID=1000 ARG GID=1000
RUN set -eux \ RUN adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER}
&& adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER} \
&& install --directory --owner=${USER} /etc/libretime /srv/libretime RUN install --directory --owner=${USER} \
/etc/libretime \
/srv/libretime
ENV LIBRETIME_CONFIG_FILEPATH=/etc/libretime/config.yml ENV LIBRETIME_CONFIG_FILEPATH=/etc/libretime/config.yml
@ -38,9 +40,8 @@ ENV LIBRETIME_CONFIG_FILEPATH=/etc/libretime/config.yml
COPY tools/packages.py /tmp/packages.py COPY tools/packages.py /tmp/packages.py
COPY shared/packages.ini /tmp/packages.ini COPY shared/packages.ini /tmp/packages.ini
RUN set -eux \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& DEBIAN_FRONTEND=noninteractive apt-get update \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
$(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \ $(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& rm -f /tmp/packages.py /tmp/packages.ini && rm -f /tmp/packages.py /tmp/packages.ini
@ -48,25 +49,23 @@ RUN set -eux \
#======================================================================================# #======================================================================================#
# Python base with ffmpeg # # Python base with ffmpeg #
#======================================================================================# #======================================================================================#
FROM python-base AS python-base-ffmpeg FROM python-base as python-base-ffmpeg
RUN set -eux \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& DEBIAN_FRONTEND=noninteractive apt-get update \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ffmpeg \ ffmpeg \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
#======================================================================================# #======================================================================================#
# Analyzer # # Analyzer #
#======================================================================================# #======================================================================================#
FROM python-base-ffmpeg AS libretime-analyzer FROM python-base-ffmpeg as libretime-analyzer
COPY tools/packages.py /tmp/packages.py COPY tools/packages.py /tmp/packages.py
COPY analyzer/packages.ini /tmp/packages.ini COPY analyzer/packages.ini /tmp/packages.ini
RUN set -eux \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& DEBIAN_FRONTEND=noninteractive apt-get update \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
$(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \ $(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& rm -f /tmp/packages.py /tmp/packages.ini && rm -f /tmp/packages.py /tmp/packages.ini
@ -83,7 +82,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY analyzer . COPY analyzer .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[sentry] pip install --editable .
# Run # Run
USER ${UID}:${GID} USER ${UID}:${GID}
@ -97,14 +96,13 @@ ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
#======================================================================================# #======================================================================================#
# Playout # # Playout #
#======================================================================================# #======================================================================================#
FROM python-base-ffmpeg AS libretime-playout FROM python-base-ffmpeg as libretime-playout
COPY tools/packages.py /tmp/packages.py COPY tools/packages.py /tmp/packages.py
COPY playout/packages.ini /tmp/packages.ini COPY playout/packages.ini /tmp/packages.ini
RUN set -eux \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& DEBIAN_FRONTEND=noninteractive apt-get update \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
$(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \ $(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& rm -f /tmp/packages.py /tmp/packages.ini && rm -f /tmp/packages.py /tmp/packages.ini
@ -122,7 +120,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY playout . COPY playout .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[sentry] pip install --editable .
# Run # Run
USER ${UID}:${GID} USER ${UID}:${GID}
@ -136,12 +134,10 @@ ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
#======================================================================================# #======================================================================================#
# API # # API #
#======================================================================================# #======================================================================================#
FROM python-base AS libretime-api FROM python-base as libretime-api
RUN set -eux \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
&& DEBIAN_FRONTEND=noninteractive apt-get update \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl \
gcc \ gcc \
libc6-dev \ libc6-dev \
libpq-dev \ libpq-dev \
@ -151,7 +147,7 @@ WORKDIR /src
COPY api/requirements.txt . COPY api/requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt pip install --no-compile gunicorn uvicorn -r requirements.txt
COPY --from=python-builder /build/shared/*.whl . COPY --from=python-builder /build/shared/*.whl .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
@ -159,7 +155,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY api . COPY api .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[prod,sentry] pip install --editable .[prod]
# Run # Run
USER ${UID}:${GID} USER ${UID}:${GID}
@ -167,7 +163,7 @@ WORKDIR /app
CMD ["/usr/local/bin/gunicorn", \ CMD ["/usr/local/bin/gunicorn", \
"--workers=4", \ "--workers=4", \
"--worker-class=libretime_api.gunicorn.Worker", \ "--worker-class=uvicorn.workers.UvicornWorker", \
"--log-file", "-", \ "--log-file", "-", \
"--bind=0.0.0.0:9001", \ "--bind=0.0.0.0:9001", \
"libretime_api.asgi"] "libretime_api.asgi"]
@ -175,12 +171,10 @@ CMD ["/usr/local/bin/gunicorn", \
ARG LIBRETIME_VERSION ARG LIBRETIME_VERSION
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
HEALTHCHECK CMD ["curl", "--fail", "http://localhost:9001/api/v2/version"]
#======================================================================================# #======================================================================================#
# Worker # # Worker #
#======================================================================================# #======================================================================================#
FROM python-base AS libretime-worker FROM python-base as libretime-worker
WORKDIR /src WORKDIR /src
@ -189,42 +183,47 @@ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt pip install --no-compile -r requirements.txt
COPY --from=python-builder /build/shared/*.whl . COPY --from=python-builder /build/shared/*.whl .
COPY --from=python-builder /build/api-client/*.whl .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile *.whl && rm -Rf *.whl pip install --no-compile *.whl && rm -Rf *.whl
COPY worker . COPY worker .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[sentry] pip install --editable .
# Run # Run
USER ${UID}:${GID} USER ${UID}:${GID}
WORKDIR /app WORKDIR /app
CMD ["/usr/local/bin/libretime-worker"] CMD ["/usr/local/bin/celery", "worker", \
"--app=libretime_worker.tasks:worker", \
"--config=libretime_worker.config", \
"--time-limit=1800", \
"--concurrency=1", \
"--loglevel=info"]
ARG LIBRETIME_VERSION ARG LIBRETIME_VERSION
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
#======================================================================================# #======================================================================================#
# Legacy # # Legacy #
#======================================================================================# #======================================================================================#
FROM php:7.4-fpm AS libretime-legacy FROM php:7.4-fpm as libretime-legacy
ENV LIBRETIME_CONFIG_FILEPATH=/etc/libretime/config.yml ENV LIBRETIME_CONFIG_FILEPATH=/etc/libretime/config.yml
ENV LIBRETIME_LOG_FILEPATH=php://stderr
# Custom user # Custom user
ARG USER=libretime ARG USER=libretime
ARG UID=1000 ARG UID=1000
ARG GID=1000 ARG GID=1000
RUN set -eux \ RUN adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER}
&& adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER} \
&& install --directory --owner=${USER} /etc/libretime /srv/libretime
RUN set -eux \ RUN install --directory --owner=${USER} \
&& DEBIAN_FRONTEND=noninteractive apt-get update \ /etc/libretime \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ /srv/libretime
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gettext \ gettext \
libcurl4-openssl-dev \ libcurl4-openssl-dev \
libfreetype6-dev \ libfreetype6-dev \
@ -269,9 +268,8 @@ COPY legacy/composer.* ./
RUN composer --no-cache install --no-progress --no-interaction --no-dev --no-autoloader RUN composer --no-cache install --no-progress --no-interaction --no-dev --no-autoloader
COPY legacy . COPY legacy .
RUN set -eux \ RUN make locale-build
&& make locale-build \ RUN composer --no-cache dump-autoload --no-interaction --no-dev
&& composer --no-cache dump-autoload --no-interaction --no-dev
# Run # Run
USER ${UID}:${GID} USER ${UID}:${GID}

View File

@ -7,57 +7,36 @@ all: setup
setup: setup:
command -v pre-commit > /dev/null && pre-commit install command -v pre-commit > /dev/null && pre-commit install
.env: # https://google.github.io/styleguide/shellguide.html
cp .env.dev .env shell-format:
shfmt -f . | xargs git ls-files | xargs shfmt -i 2 -ci -sr -kp -w
dev-certs: shell-check:
rm -f dev/certs/fake.* shfmt -f . | xargs git ls-files | xargs shfmt -i 2 -ci -sr -kp -d
openssl req -x509 \ shfmt -f . | xargs git ls-files | xargs shellcheck --color=always --severity=$${SEVERITY:-style}
-newkey rsa:2048 \
-days 365 \
-nodes \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
-keyout dev/certs/fake.key \
-out dev/certs/fake.crt
cat dev/certs/fake.{key,crt} > dev/certs/fake.pem
dev: .env dev-certs
DOCKER_BUILDKIT=1 docker compose build
docker compose run --rm legacy make build
docker compose run --rm api libretime-api migrate
docker compose up -d
.PHONY: VERSION .PHONY: VERSION
VERSION: VERSION:
tools/version.sh tools/version.sh
changelog:
tools/changelog.sh
.PHONY: tarball .PHONY: tarball
tarball: VERSION tarball: VERSION
$(MAKE) -C legacy build $(MAKE) -C legacy build
cd .. && tar -czf libretime-$(shell cat VERSION | tr -d [:blank:]).tar.gz \ cd .. && tar -czf libretime-$(shell cat VERSION | tr -d [:blank:]).tar.gz \
--owner=root --group=root \ --owner=root --group=root \
--exclude-vcs \ --exclude-vcs \
libretime/analyzer \ --exclude .codespellignore \
libretime/api \ --exclude .git* \
libretime/api-client \ --exclude .pre-commit-config.yaml \
libretime/docs \ --exclude dev_tools \
libretime/installer \ --exclude jekyll.sh \
libretime/legacy \
--exclude legacy/vendor/phing \ --exclude legacy/vendor/phing \
--exclude legacy/vendor/simplepie/simplepie/tests \ --exclude legacy/vendor/simplepie/simplepie/tests \
libretime/playout \ libretime
libretime/shared \
libretime/tools \
libretime/worker \
libretime/CHANGELOG.md \
libretime/install \
libretime/LICENSE \
libretime/Makefile \
libretime/README.md \
libretime/SECURITY.md \
libretime/VERSION
mv ../libretime-*.tar.gz . mv ../libretime-*.tar.gz .
sha256sum libretime-*.tar.gz > sha256sums.txt
# Only clean subdirs # Only clean subdirs
clean: clean:
@ -65,13 +44,4 @@ clean:
docs-lint: docs-lint:
vale sync vale sync
vale docs vale docs website/src/pages
website:
git clone git@github.com:libretime/website.git
website/node_modules: website
yarn --cwd website install
docs-dev: website website/node_modules
DOCS_PATH="../docs" yarn --cwd website start

View File

@ -1,4 +1,4 @@
# [![LibreTime](https://github.com/libretime/website/blob/main/static/img/logo-512px.png)](https://github.com/libretime/libretime) ![](website/static/img/logo-512px.png)
[![Financial Contributors on Open Collective](https://opencollective.com/libretime/all/badge.svg?label=financial+contributors)](https://opencollective.com/libretime) [![Financial Contributors on Open Collective](https://opencollective.com/libretime/all/badge.svg?label=financial+contributors)](https://opencollective.com/libretime)
@ -9,15 +9,33 @@ It is managed by a friendly inclusive community of stations from around the
globe that use, document and improve LibreTime. Join us in fixing bugs and in globe that use, document and improve LibreTime. Join us in fixing bugs and in
defining how we manage the codebase going forward. defining how we manage the codebase going forward.
Check out the [documentation](https://libretime.org/docs/) for more information and We are currently ramping up development on this repository.
Check out the [documentation](https://libretime.org) for more information and
start broadcasting! start broadcasting!
Please note that LibreTime is released with a [Contributor Code Please note that LibreTime is released with a [Contributor Code
of Conduct](https://github.com/libretime/organization/blob/main/CODE_OF_CONDUCT.md). of Conduct](https://github.com/libretime/organization/blob/main/CODE_OF_CONDUCT.md).
By participating in this project you agree to abide by its terms. By participating in this project you agree to abide by its terms.
You can find details about our development process in the Please submit enhancements, bug-fixes or comments via GitHub.
[contributing](./CONTRIBUTING.md) guide.
## Development Process
The LibreTime follows the standardized [Collective Code Construction
Contract (C4)](https://rfc.zeromq.org/spec:42/C4/). Its abstract is
provided here.
> C4 provides a standard process for contributing, evaluating and
> discussing improvements on software projects. It defines specific
> technical requirements for projects like a style guide, unit tests,
> git and similar platforms. It also establishes different personas
> for projects, with clear and distinct duties. C4 specifies a process
> for documenting and discussing issues including seeking consensus
> and clear descriptions, use of "pull requests" and systematic reviews.
The full text of the contract is licensed under the GPL and available at
the above link courtesy of the [ZeroMQ community](https://zeromq.org/).
## Support ## Support
@ -26,8 +44,7 @@ we have a forum at [discourse.libretime.org](https://discourse.libretime.org).
We are moving towards using the forum to provide community support and reserving We are moving towards using the forum to provide community support and reserving
the github issue queue for confirmed bugs and well-formed feature requests. the github issue queue for confirmed bugs and well-formed feature requests.
You can also contact us through [Matrix You can also contact us through our [Mattermost instance](https://chat.libretime.org)
(#libretime:matrix.org)](https://matrix.to/#/#libretime:matrix.org)
where you can talk with other users and developers. where you can talk with other users and developers.
## Contributors ## Contributors
@ -52,8 +69,23 @@ Become a financial contributor and help us sustain our community on
[Support](https://opencollective.com/libretime/contribute) this project with [Support](https://opencollective.com/libretime/contribute) this project with
your organization. Your logo will show up here with a link to your website. your organization. Your logo will show up here with a link to your website.
<a href="https://opencollective.com/libretime"> <a href="https://opencollective.com/libretime/organization/0/website">
<img src="https://opencollective.com/libretime/organizations.svg?width=890"> <img src="https://opencollective.com/libretime/organization/0/avatar.svg">
</a>
<a href="https://opencollective.com/libretime/organization/1/website">
<img src="https://opencollective.com/libretime/organization/1/avatar.svg">
</a>
<a href="https://opencollective.com/libretime/organization/2/website">
<img src="https://opencollective.com/libretime/organization/2/avatar.svg">
</a>
<a href="https://opencollective.com/libretime/organization/3/website">
<img src="https://opencollective.com/libretime/organization/3/avatar.svg">
</a>
<a href="https://opencollective.com/libretime/organization/4/website">
<img src="https://opencollective.com/libretime/organization/4/avatar.svg">
</a>
<a href="https://opencollective.com/libretime/organization/5/website">
<img src="https://opencollective.com/libretime/organization/5/avatar.svg">
</a> </a>
## License ## License
@ -67,6 +99,6 @@ version 3 of the License.
Copyright (c) 2011-2017 Sourcefabric z.ú. Copyright (c) 2011-2017 Sourcefabric z.ú.
Copyright (c) 2017-2023 LibreTime Community Copyright (c) 2017-2022 LibreTime Community
Please refer to the [LEGACY](./LEGACY.md) file for more information. Please refer to the [LEGACY](./LEGACY.md) file for more information.

View File

@ -1,14 +0,0 @@
# Security Policy
## Reporting a Vulnerability
**Please do not use GitHub issues for security-sensitive communication.**
The LibreTime maintainers ask that known and suspected vulnerabilities to be privately and responsibly disclosed by:
- sending all the required detail to [security@libretime.org](security@libretime.org),
- or by filling a [security advisory on Github](https://github.com/libretime/libretime/security/advisories/new).
A LibreTime maintainer will acknowledged the report within 3 working days.
We aim to provide a security patch within 30 days, after this period the report will be disclosed to the public. The security patch will be distributed for the [maintained versions of LibreTime](https://libretime.org/docs/releases/#distributions-releases-support).

24
Vagrantfile vendored
View File

@ -6,7 +6,7 @@
# export VAGRANT_NO_PORT_FORWARDING=true # export VAGRANT_NO_PORT_FORWARDING=true
# export VAGRANT_CPUS=4 # export VAGRANT_CPUS=4
# export VAGRANT_MEMORY=4096 # export VAGRANT_MEMORY=4096
# vagrant up bullseye # vagrant up buster
# #
Vagrant.configure('2') do |config| Vagrant.configure('2') do |config|
@ -79,6 +79,7 @@ Vagrant.configure('2') do |config|
LIBRETIME_POSTGRESQL_PASSWORD=libretime \ LIBRETIME_POSTGRESQL_PASSWORD=libretime \
LIBRETIME_RABBITMQ_PASSWORD=libretime \ LIBRETIME_RABBITMQ_PASSWORD=libretime \
bash install \ bash install \
--listen-port 8080 \
--in-place \ --in-place \
http://192.168.10.100:8080 http://192.168.10.100:8080
@ -98,6 +99,12 @@ Vagrant.configure('2') do |config|
setup_libretime(os, "debian.sh") setup_libretime(os, "debian.sh")
end end
config.vm.define 'bionic' do |os|
os.vm.box = 'bento/ubuntu-18.04'
setup_nfs(config)
setup_libretime(os, 'debian.sh')
end
config.vm.define 'bullseye' do |os| config.vm.define 'bullseye' do |os|
os.vm.box = 'debian/bullseye64' os.vm.box = 'debian/bullseye64'
config.vm.provider 'virtualbox' do |v, override| config.vm.provider 'virtualbox' do |v, override|
@ -106,4 +113,19 @@ Vagrant.configure('2') do |config|
setup_nfs(config, 4) setup_nfs(config, 4)
setup_libretime(os, 'debian.sh') setup_libretime(os, 'debian.sh')
end end
config.vm.define 'buster' do |os|
os.vm.box = 'debian/buster64'
config.vm.provider 'virtualbox' do |v, override|
override.vm.box = 'bento/debian-10'
end
setup_nfs(config)
setup_libretime(os, 'debian.sh')
end
config.vm.define 'centos' do |os|
os.vm.box = 'centos/8'
setup_nfs(config)
setup_libretime(os, 'centos.sh', '--selinux')
end
end end

View File

@ -4,10 +4,11 @@ include ../tools/python.mk
PIP_INSTALL := \ PIP_INSTALL := \
--editable ../shared \ --editable ../shared \
--editable .[dev,sentry] --editable .[dev]
PYLINT_ARG := libretime_analyzer tests || true PYLINT_ARG := libretime_analyzer tests || true
MYPY_ARG := libretime_analyzer tests || true MYPY_ARG := libretime_analyzer tests || true
BANDIT_ARG := libretime_analyzer || true BANDIT_ARG := libretime_analyzer || true
PYTEST_ARG := --cov=libretime_analyzer tests
format: .format format: .format
lint: .format-check .pylint .mypy .bandit lint: .format-check .pylint .mypy .bandit
@ -16,5 +17,4 @@ fixtures:
bash tests/fixtures/generate.sh bash tests/fixtures/generate.sh
test: fixtures .pytest test: fixtures .pytest
test-coverage: fixtures .coverage
clean: .clean clean: .clean

View File

@ -10,6 +10,7 @@ PrivateTmp=true
PrivateUsers=true PrivateUsers=true
ProtectClock=true ProtectClock=true
ProtectControlGroups=true ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true ProtectHostname=true
ProtectKernelLogs=true ProtectKernelLogs=true
ProtectKernelModules=true ProtectKernelModules=true
@ -17,7 +18,6 @@ ProtectKernelTunables=true
ProtectProc=invisible ProtectProc=invisible
ProtectSystem=full ProtectSystem=full
Environment=PYTHONOPTIMIZE=2
Environment=LIBRETIME_CONFIG_FILEPATH=@@CONFIG_FILEPATH@@ Environment=LIBRETIME_CONFIG_FILEPATH=@@CONFIG_FILEPATH@@
Environment=LIBRETIME_LOG_FILEPATH=@@LOG_DIR@@/analyzer.log Environment=LIBRETIME_LOG_FILEPATH=@@LOG_DIR@@/analyzer.log
WorkingDirectory=@@WORKING_DIR@@/analyzer WorkingDirectory=@@WORKING_DIR@@/analyzer

View File

@ -1,4 +0,0 @@
from importlib.metadata import version as get_version
PACKAGE = __name__
VERSION = get_version(__name__)

View File

@ -1,20 +1,15 @@
import logging
import os
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import click import click
from libretime_shared.cli import cli_config_options, cli_logging_options from libretime_shared.cli import cli_config_options, cli_logging_options
from libretime_shared.config import DEFAULT_ENV_PREFIX from libretime_shared.config import DEFAULT_ENV_PREFIX
from libretime_shared.logging import setup_logger from libretime_shared.logging import level_from_name, setup_logger
from . import PACKAGE, VERSION
from .config import Config from .config import Config
from .message_listener import MessageListener from .message_listener import MessageListener
from .status_reporter import StatusReporter from .status_reporter import StatusReporter
logger = logging.getLogger(__name__)
VERSION = "1.0" VERSION = "1.0"
DEFAULT_RETRY_QUEUE_FILEPATH = Path("retry_queue") DEFAULT_RETRY_QUEUE_FILEPATH = Path("retry_queue")
@ -38,19 +33,9 @@ def cli(
""" """
Run analyzer. Run analyzer.
""" """
setup_logger(log_level, log_filepath) setup_logger(level_from_name(log_level), log_filepath)
config = Config(config_filepath) config = Config(config_filepath)
if "SENTRY_DSN" in os.environ:
logger.info("installing sentry")
# pylint: disable=import-outside-toplevel
import sentry_sdk
sentry_sdk.init(
traces_sample_rate=1.0,
release=f"{PACKAGE}@{VERSION}",
)
# Start up the StatusReporter process # Start up the StatusReporter process
StatusReporter.start_thread(retry_queue_filepath) StatusReporter.start_thread(retry_queue_filepath)

View File

@ -1,17 +1,15 @@
import json import json
import logging
import signal import signal
import time import time
from queue import Queue from queue import Queue
import pika import pika
from loguru import logger
from .config import Config from .config import Config
from .pipeline import Pipeline, PipelineOptions, PipelineStatus from .pipeline import Pipeline, PipelineStatus
from .status_reporter import StatusReporter from .status_reporter import StatusReporter
logger = logging.getLogger(__name__)
EXCHANGE = "airtime-uploads" EXCHANGE = "airtime-uploads"
EXCHANGE_TYPE = "topic" EXCHANGE_TYPE = "topic"
ROUTING_KEY = "" ROUTING_KEY = ""
@ -100,7 +98,7 @@ class MessageListener:
Here we parse the message, spin up an analyzer process, and report the Here we parse the message, spin up an analyzer process, and report the
metadata back to the Airtime web application (or report an error). metadata back to the Airtime web application (or report an error).
""" """
logger.info("Received '%s' on routing_key '%s'", body, method_frame.routing_key) logger.info(f" - Received '{body}' on routing_key '{method_frame.routing_key}'")
audio_file_path = "" audio_file_path = ""
# final_file_path = "" # final_file_path = ""
@ -113,19 +111,17 @@ class MessageListener:
body = body.decode() body = body.decode()
except (UnicodeDecodeError, AttributeError): except (UnicodeDecodeError, AttributeError):
pass pass
msg_dict: dict = json.loads(body) msg_dict = json.loads(body)
file_id = msg_dict["file_id"] file_id = msg_dict["file_id"]
audio_file_path = msg_dict["tmp_file_path"] audio_file_path = msg_dict["tmp_file_path"]
original_filename = msg_dict["original_filename"] original_filename = msg_dict["original_filename"]
import_directory = msg_dict["import_directory"] import_directory = msg_dict["import_directory"]
options = msg_dict.get("options", {})
metadata = MessageListener.spawn_analyzer_process( metadata = MessageListener.spawn_analyzer_process(
audio_file_path, audio_file_path,
import_directory, import_directory,
original_filename, original_filename,
options,
) )
callback_url = f"{self.config.general.public_url}/rest/media/{file_id}" callback_url = f"{self.config.general.public_url}/rest/media/{file_id}"
@ -165,7 +161,6 @@ class MessageListener:
audio_file_path, audio_file_path,
import_directory, import_directory,
original_filename, original_filename,
options: dict,
): ):
metadata = {} metadata = {}
@ -176,11 +171,10 @@ class MessageListener:
audio_file_path, audio_file_path,
import_directory, import_directory,
original_filename, original_filename,
PipelineOptions(**options),
) )
metadata = queue.get() metadata = queue.get()
except Exception as exception: except Exception as exception:
logger.exception("Analyzer pipeline exception: %s", exception) logger.exception(f"Analyzer pipeline exception: {exception}")
metadata["import_status"] = PipelineStatus.FAILED metadata["import_status"] = PipelineStatus.FAILED
# Ensure our queue doesn't fill up and block due to unexpected behavior. Defensive code. # Ensure our queue doesn't fill up and block due to unexpected behavior. Defensive code.

View File

@ -1 +1 @@
from .pipeline import Pipeline, PipelineOptions, PipelineStatus from .pipeline import Pipeline, PipelineStatus

View File

@ -36,7 +36,7 @@ def probe_replaygain(filepath: Path) -> Optional[float]:
""" """
Probe replaygain will probe the given audio file and return the replaygain if available. Probe replaygain will probe the given audio file and return the replaygain if available.
""" """
cmd = _ffprobe("-i", filepath, errors="backslashreplace") cmd = _ffprobe("-i", filepath)
track_gain_match = _PROBE_REPLAYGAIN_RE.search(cmd.stderr) track_gain_match = _PROBE_REPLAYGAIN_RE.search(cmd.stderr)
@ -75,7 +75,8 @@ def compute_silences(filepath: Path) -> List[Tuple[float, float]]:
cmd = _ffmpeg( cmd = _ffmpeg(
*("-i", filepath), *("-i", filepath),
"-vn", "-vn",
*("-filter", "highpass=frequency=80,silencedetect=noise=-60dB:duration=0.9"), *("-filter", "highpass=frequency=1000"),
*("-filter", "silencedetect=noise=0.15:duration=1"),
) )
starts, ends = [], [] starts, ends = [], []
@ -93,8 +94,9 @@ def compute_silences(filepath: Path) -> List[Tuple[float, float]]:
end = float(match.group(2)) end = float(match.group(2))
ends.append(end) ends.append(end)
# If one end is missing, set the last silence ending to infinity, and # ffmpeg v3 (bionic) does not warn about silence end when the track ends.
# clamp it to the track duration before using this value. # Set the last silence ending to infinity, and clamp it to the track duration before
# using this value.
if len(starts) - 1 == len(ends): if len(starts) - 1 == len(ends):
ends.append(inf) ends.append(inf)

View File

@ -1,7 +1,6 @@
import logging from subprocess import PIPE, CalledProcessError, CompletedProcess, run
from subprocess import CalledProcessError, CompletedProcess, run
logger = logging.getLogger(__name__) from loguru import logger
def run_(*args, **kwargs) -> CompletedProcess: def run_(*args, **kwargs) -> CompletedProcess:
@ -9,14 +8,15 @@ def run_(*args, **kwargs) -> CompletedProcess:
return run( return run(
args, args,
check=True, check=True,
capture_output=True, stdout=PIPE,
text=True, stderr=PIPE,
universal_newlines=True,
**kwargs, **kwargs,
) )
except OSError as exception: # executable was not found except OSError as exception: # executable was not found
cmd = args[0] cmd = args[0]
logger.warning("Failed to run: %s - %s. Is %s installed?", cmd, exception, cmd) logger.warning(f"Failed to run: {cmd} - {exception}. Is {cmd} installed?")
raise exception raise exception
except CalledProcessError as exception: # returned an error code except CalledProcessError as exception: # returned an error code

View File

@ -1,18 +1,18 @@
import logging
from datetime import timedelta from datetime import timedelta
from math import isclose from math import isclose
from subprocess import CalledProcessError from subprocess import CalledProcessError
from typing import Any, Dict from typing import Any, Dict
from loguru import logger
from ._ffmpeg import compute_silences, probe_duration from ._ffmpeg import compute_silences, probe_duration
logger = logging.getLogger(__name__)
def analyze_cuepoint(filepath: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
"""
Extracts the cuein and cueout times along and sets the file duration using ffmpeg.
"""
def analyze_duration(filepath: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
"""
Extracts the file duration using ffmpeg.
"""
try: try:
duration = probe_duration(filepath) duration = probe_duration(filepath)
@ -30,23 +30,7 @@ def analyze_duration(filepath: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
metadata["length"] = str(timedelta(seconds=duration)) metadata["length"] = str(timedelta(seconds=duration))
metadata["cuein"] = 0.0 metadata["cuein"] = 0.0
metadata["cueout"] = duration metadata["cueout"] = duration
except (CalledProcessError, OSError):
pass
return metadata
def analyze_cuepoint(filepath: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
"""
Extracts the cuein and cueout times using ffmpeg.
This step must run after the 'analyze_duration' step.
"""
# Duration has been computed in the 'analyze_duration' step
duration = metadata["length_seconds"]
try:
silences = compute_silences(filepath) silences = compute_silences(filepath)
if len(silences) > 2: if len(silences) > 2:

View File

@ -1,26 +1,10 @@
import logging
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
import mutagen import mutagen
from libretime_shared.files import compute_md5 from libretime_shared.files import compute_md5
from mutagen.easyid3 import EasyID3 from loguru import logger
logger = logging.getLogger(__name__)
def flatten(xss):
return [x for xs in xss for x in xs]
def comment_get(id3, _):
comments = [v.text for k, v in id3.items() if "COMM" in k or "comment" in k]
return flatten(comments)
EasyID3.RegisterKey("comment", comment_get)
def analyze_metadata(filepath_: str, metadata: Dict[str, Any]): def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
@ -40,7 +24,7 @@ def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
# Get audio file metadata # Get audio file metadata
extracted = mutagen.File(filepath, easy=True) extracted = mutagen.File(filepath, easy=True)
if extracted is None: if extracted is None:
logger.warning("no metadata were extracted for %s", filepath) logger.warning(f"no metadata were extracted for {filepath}")
return metadata return metadata
metadata["mime"] = extracted.mime[0] metadata["mime"] = extracted.mime[0]
@ -85,36 +69,34 @@ def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
except (AttributeError, KeyError, IndexError): except (AttributeError, KeyError, IndexError):
pass pass
extracted_tags_mapping = [ extracted_tags_mapping = {
("title", "track_title"), "title": "track_title",
("artist", "artist_name"), "artist": "artist_name",
("album", "album_title"), "album": "album_title",
("bpm", "bpm"), "bpm": "bpm",
("composer", "composer"), "composer": "composer",
("conductor", "conductor"), "conductor": "conductor",
("copyright", "copyright"), "copyright": "copyright",
("comment", "comment"), "comment": "comment",
("comment", "comments"), "encoded_by": "encoder",
("comment", "description"), "genre": "genre",
("encoded_by", "encoder"), "isrc": "isrc",
("genre", "genre"), "label": "label",
("isrc", "isrc"), "organization": "label",
("label", "label"), # "length": "length",
("organization", "label"), "language": "language",
# ("length", "length"), "last_modified": "last_modified",
("language", "language"), "mood": "mood",
("last_modified", "last_modified"), "bit_rate": "bit_rate",
("mood", "mood"), "replay_gain": "replaygain",
("bit_rate", "bit_rate"), # "tracknumber": "track_number",
("replay_gain", "replaygain"), # "track_total": "track_total",
# ("tracknumber", "track_number"), "website": "website",
# ("track_total", "track_total"), "date": "year",
("website", "website"), # "mime_type": "mime",
("date", "year"), }
# ("mime_type", "mime"),
]
for extracted_key, metadata_key in extracted_tags_mapping: for extracted_key, metadata_key in extracted_tags_mapping.items():
try: try:
metadata[metadata_key] = extracted[extracted_key] metadata[metadata_key] = extracted[extracted_key]
if isinstance(metadata[metadata_key], list): if isinstance(metadata[metadata_key], list):

View File

@ -1,10 +1,9 @@
import logging
from subprocess import CalledProcessError from subprocess import CalledProcessError
from typing import Any, Dict from typing import Any, Dict
from ._liquidsoap import _liquidsoap from loguru import logger
logger = logging.getLogger(__name__) from ._liquidsoap import _liquidsoap
class UnplayableFileError(Exception): class UnplayableFileError(Exception):
@ -27,6 +26,6 @@ def analyze_playability(filename: str, metadata: Dict[str, Any]):
raise UnplayableFileError() from exception raise UnplayableFileError() from exception
except OSError as exception: # liquidsoap was not found except OSError as exception: # liquidsoap was not found
logger.warning("Failed to run: %s. Is liquidsoap installed?", exception) logger.warning(f"Failed to run: {exception}. Is liquidsoap installed?")
return metadata return metadata

View File

@ -1,9 +1,8 @@
import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
logger = logging.getLogger(__name__) from loguru import logger
MAX_DIR_LEN = 48 MAX_DIR_LEN = 48
MAX_FILE_LEN = 48 MAX_FILE_LEN = 48
@ -43,12 +42,12 @@ def organise_file(
return metadata return metadata
dest_path = dest_path.with_name(f"{dest_path.stem}_{uuid4()}{dest_path.suffix}") dest_path = dest_path.with_name(f"{dest_path.stem}_{uuid4()}{dest_path.suffix}")
logger.warning("found existing file, using new filepath %s", dest_path) logger.warning(f"found existing file, using new filepath {dest_path}")
# Import # Import
dest_path.parent.mkdir(parents=True, exist_ok=True) dest_path.parent.mkdir(parents=True, exist_ok=True)
logger.debug("moving %s to %s", filepath, dest_path) logger.debug(f"moving {filepath} to {dest_path}")
shutil.move(filepath, dest_path) shutil.move(filepath, dest_path)
metadata["full_path"] = str(dest_path) metadata["full_path"] = str(dest_path)

View File

@ -1,22 +1,21 @@
import logging
from enum import Enum from enum import Enum
from queue import Queue from queue import Queue
from typing import Any, Dict, Protocol from typing import Any, Dict
from pydantic import BaseModel from loguru import logger
from typing_extensions import Protocol
from .analyze_cuepoint import analyze_cuepoint, analyze_duration from .analyze_cuepoint import analyze_cuepoint
from .analyze_metadata import analyze_metadata from .analyze_metadata import analyze_metadata
from .analyze_playability import UnplayableFileError, analyze_playability from .analyze_playability import UnplayableFileError, analyze_playability
from .analyze_replaygain import analyze_replaygain from .analyze_replaygain import analyze_replaygain
from .organise_file import organise_file from .organise_file import organise_file
logger = logging.getLogger(__name__)
class Step(Protocol): class Step(Protocol):
@staticmethod @staticmethod
def __call__(filename: str, metadata: Dict[str, Any]): ... def __call__(filename: str, metadata: Dict[str, Any]):
...
class PipelineStatus(int, Enum): class PipelineStatus(int, Enum):
@ -25,10 +24,6 @@ class PipelineStatus(int, Enum):
FAILED = 2 FAILED = 2
class PipelineOptions(BaseModel):
analyze_cue_points: bool = False
class Pipeline: class Pipeline:
"""Analyzes and imports an audio file into the Airtime library. """Analyzes and imports an audio file into the Airtime library.
@ -39,11 +34,10 @@ class Pipeline:
@staticmethod @staticmethod
def run_analysis( def run_analysis(
queue: Queue, queue,
audio_file_path: str, audio_file_path,
import_directory: str, import_directory,
original_filename: str, original_filename,
options: PipelineOptions,
): ):
"""Analyze and import an audio file, and put all extracted metadata into queue. """Analyze and import an audio file, and put all extracted metadata into queue.
@ -84,9 +78,7 @@ class Pipeline:
# First, we extract the ID3 tags and other metadata: # First, we extract the ID3 tags and other metadata:
metadata = {} metadata = {}
metadata = analyze_metadata(audio_file_path, metadata) metadata = analyze_metadata(audio_file_path, metadata)
metadata = analyze_duration(audio_file_path, metadata) metadata = analyze_cuepoint(audio_file_path, metadata)
if options.analyze_cue_points:
metadata = analyze_cuepoint(audio_file_path, metadata)
metadata = analyze_replaygain(audio_file_path, metadata) metadata = analyze_replaygain(audio_file_path, metadata)
metadata = analyze_playability(audio_file_path, metadata) metadata = analyze_playability(audio_file_path, metadata)

View File

@ -1,6 +1,5 @@
import collections import collections
import json import json
import logging
import pickle import pickle
import queue import queue
import threading import threading
@ -8,10 +7,9 @@ import time
from urllib.parse import urlparse from urllib.parse import urlparse
import requests import requests
from loguru import logger
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
logger = logging.getLogger(__name__)
class PicklableHttpRequest: class PicklableHttpRequest:
def __init__(self, method, url, api_key, data): def __init__(self, method, url, api_key, data):
@ -57,7 +55,7 @@ def process_http_requests(ipc_queue, http_retry_queue_path):
# If we fail to unpickle a saved queue of failed HTTP requests, then we'll just log an error # If we fail to unpickle a saved queue of failed HTTP requests, then we'll just log an error
# and continue because those HTTP requests are lost anyways. The pickled file will be # and continue because those HTTP requests are lost anyways. The pickled file will be
# overwritten the next time the analyzer is shut down too. # overwritten the next time the analyzer is shut down too.
logger.error("Failed to unpickle %s. Continuing...", http_retry_queue_path) logger.error(f"Failed to unpickle {http_retry_queue_path}. Continuing...")
while True: while True:
try: try:
@ -88,12 +86,10 @@ def process_http_requests(ipc_queue, http_retry_queue_path):
with open(http_retry_queue_path, "wb") as pickle_file: with open(http_retry_queue_path, "wb") as pickle_file:
pickle.dump(retry_queue, pickle_file) pickle.dump(retry_queue, pickle_file)
return return
except ( except Exception as exception: # Terrible top-level exception handler to prevent the thread from dying, just in case.
Exception
) as exception: # Terrible top-level exception handler to prevent the thread from dying, just in case.
if shutdown: if shutdown:
return return
logger.exception("Unhandled exception in StatusReporter %s", exception) logger.exception(f"Unhandled exception in StatusReporter {exception}")
logger.info("Restarting StatusReporter thread") logger.info("Restarting StatusReporter thread")
time.sleep(2) # Throttle it time.sleep(2) # Throttle it
@ -118,7 +114,7 @@ def send_http_request(picklable_request: PicklableHttpRequest, retry_queue):
# The request failed with an error 500 probably, so let's check if Airtime and/or # The request failed with an error 500 probably, so let's check if Airtime and/or
# the web server are broken. If not, then our request was probably causing an # the web server are broken. If not, then our request was probably causing an
# error 500 in the media API (ie. a bug), so there's no point in retrying it. # error 500 in the media API (ie. a bug), so there's no point in retrying it.
logger.exception("HTTP request failed: %s", exception) logger.exception(f"HTTP request failed: {exception}")
parsed_url = urlparse(exception.response.request.url) parsed_url = urlparse(exception.response.request.url)
if is_web_server_broken(parsed_url.scheme + "://" + parsed_url.netloc): if is_web_server_broken(parsed_url.scheme + "://" + parsed_url.netloc):
# If the web server is having problems, retry the request later: # If the web server is having problems, retry the request later:
@ -128,12 +124,11 @@ def send_http_request(picklable_request: PicklableHttpRequest, retry_queue):
# notified by sentry. # notified by sentry.
except requests.exceptions.ConnectionError as exception: except requests.exceptions.ConnectionError as exception:
logger.exception( logger.exception(
"HTTP request failed due to a connection error, retrying later: %s", f"HTTP request failed due to a connection error. Retrying later. {exception}"
exception,
) )
retry_queue.append(picklable_request) # Retry it later retry_queue.append(picklable_request) # Retry it later
except Exception as exception: except Exception as exception:
logger.exception("HTTP request failed with unhandled exception. %s", exception) logger.exception(f"HTTP request failed with unhandled exception. {exception}")
# Don't put the request into the retry queue, just give up on this one. # Don't put the request into the retry queue, just give up on this one.
# I'm doing this to protect against us getting some pathological request # I'm doing this to protect against us getting some pathological request
# that breaks our code. I don't want us pickling data that potentially # that breaks our code. I don't want us pickling data that potentially
@ -215,7 +210,7 @@ class StatusReporter:
audio_metadata["import_status"] = import_status audio_metadata["import_status"] = import_status
audio_metadata["comment"] = reason # hack attack audio_metadata["comment"] = reason # hack attack
put_payload = json.dumps(audio_metadata) put_payload = json.dumps(audio_metadata)
# logger.debug("sending http put with payload: %s", put_payload) # logger.debug("sending http put with payload: " + put_payload)
StatusReporter._send_http_request( StatusReporter._send_http_request(
PicklableHttpRequest( PicklableHttpRequest(

View File

@ -1,16 +1,32 @@
# This file contains a list of package dependencies. # This file contains a list of package dependencies.
[python] [python]
python3 = focal, bullseye, jammy, bookworm python3 = buster, bullseye, bookworm, bionic, focal, jammy
python3-pip = focal, bullseye, jammy, bookworm python3-pip = buster, bullseye, bookworm, bionic, focal, jammy
python3-pika = buster, bullseye, bookworm, bionic, focal, jammy
[liquidsoap] [liquidsoap]
# https://github.com/savonet/liquidsoap/blob/main/CHANGES.md # https://github.com/savonet/liquidsoap/blob/main/CHANGES.md
liquidsoap = focal, bullseye, jammy, bookworm liquidsoap-plugin-alsa = bionic
liquidsoap-plugin-ao = bionic
liquidsoap-plugin-ogg = bionic
liquidsoap-plugin-portaudio = bionic
# Already recommended packages in bionic
# See `apt show liquidsoap`
; liquidsoap-plugin-faad = bionic
; liquidsoap-plugin-flac = bionic
; liquidsoap-plugin-icecast = bionic
; liquidsoap-plugin-lame = bionic
; liquidsoap-plugin-mad = bionic
; liquidsoap-plugin-pulseaudio = bionic
; liquidsoap-plugin-taglib = bionic
; liquidsoap-plugin-voaacenc = bionic
; liquidsoap-plugin-vorbis = bionic
liquidsoap = buster, bullseye, bookworm, bionic, focal, jammy
[ffmpeg] [ffmpeg]
# Detect duration, silences and replaygain # Detect duration, silences and replaygain
ffmpeg = focal, bullseye, jammy, bookworm ffmpeg = buster, bullseye, bookworm, bionic, focal, jammy
[=development] [=development]
# Generate fixtures # Generate fixtures
ffmpeg = focal, bullseye, jammy, bookworm ffmpeg = buster, bullseye, bookworm, bionic, focal, jammy

View File

@ -1,8 +1,3 @@
[tool.isort]
profile = "black"
combine_as_imports = true
known_first_party = ["libretime_analyzer"]
[tool.pylint.messages_control] [tool.pylint.messages_control]
extension-pkg-whitelist = "pydantic" extension-pkg-whitelist = "pydantic"
disable = [ disable = [
@ -11,13 +6,13 @@ disable = [
"missing-module-docstring", "missing-module-docstring",
] ]
[tool.pylint.format]
disable = "logging-fstring-interpolation"
[tool.pytest.ini_options] [tool.pytest.ini_options]
log_cli = true log_cli = true
log_cli_level = "DEBUG" log_cli_level = "DEBUG"
[tool.coverage.run]
source = ["libretime_analyzer"]
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"

View File

@ -1,6 +1,6 @@
# Please do not edit this file, edit the setup.py file! # Please do not edit this file, edit the setup.py file!
# This file is auto-generated by tools/extract_requirements.py. # This file is auto-generated by tools/extract_requirements.py.
mutagen>=1.45.1,<1.48 mutagen>=1.45.1,<1.46
pika>=1.0.0,<1.4 pika>=1.0.0,<1.4
requests>=2.32.2,<2.33 requests>=2.25.1,<2.29
typing_extensions typing_extensions

View File

@ -1,10 +1,8 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
version = "4.2.0" # x-release-please-version
setup( setup(
name="libretime-analyzer", name="libretime-analyzer",
version=version, version="3.0.0-beta.2",
description="Libretime Analyzer", description="Libretime Analyzer",
author="LibreTime Contributors", author="LibreTime Contributors",
url="https://github.com/libretime/libretime", url="https://github.com/libretime/libretime",
@ -20,20 +18,17 @@ setup(
"libretime-analyzer=libretime_analyzer.main:cli", "libretime-analyzer=libretime_analyzer.main:cli",
] ]
}, },
python_requires=">=3.8", python_requires=">=3.6",
install_requires=[ install_requires=[
"mutagen>=1.45.1,<1.48", "mutagen>=1.45.1,<1.46",
"pika>=1.0.0,<1.4", "pika>=1.0.0,<1.4",
"requests>=2.32.2,<2.33", "requests>=2.25.1,<2.29",
"typing_extensions", "typing_extensions",
], ],
extras_require={ extras_require={
"dev": [ "dev": [
"distro>=1.8.0,<2", "distro",
"types-requests>=2.31.0,<3", "types-requests",
],
"sentry": [
"sentry-sdk>=1.15.0,<2",
], ],
}, },
zip_safe=False, zip_safe=False,

View File

@ -2,11 +2,11 @@ import shutil
from pathlib import Path from pathlib import Path
import pytest import pytest
from libretime_shared.logging import setup_logger from libretime_shared.logging import TRACE, setup_logger
from .fixtures import fixtures_path from .fixtures import fixtures_path
setup_logger("debug") setup_logger(TRACE)
AUDIO_FILENAME = "s1-stereo-tagged.mp3" AUDIO_FILENAME = "s1-stereo-tagged.mp3"
AUDIO_FILE = fixtures_path / AUDIO_FILENAME AUDIO_FILE = fixtures_path / AUDIO_FILENAME

View File

@ -23,28 +23,28 @@ FILES = [
# 8s -> 9s: silence # 8s -> 9s: silence
# 9s -> 12s: musik # 9s -> 12s: musik
# 12s -> 15s: pink noise fade out # 12s -> 15s: pink noise fade out
Fixture(here / "s1-jointstereo.mp3", 15.0, 1.4, 15.0, -5.9 ), Fixture(here / "s1-jointstereo.mp3", 15.0, 6.0, 13.0, -5.9 ),
Fixture(here / "s1-mono.mp3", 15.0, 1.5, 15.0, -2.0 ), Fixture(here / "s1-mono.mp3", 15.0, 6.0, 13.0, -2.0 ),
Fixture(here / "s1-stereo.mp3", 15.0, 1.4, 15.0, -5.9 ), Fixture(here / "s1-stereo.mp3", 15.0, 6.0, 13.0, -5.9 ),
Fixture(here / "s1-mono-12.mp3", 15.0, 1.2, 15.0, +7.0 ), Fixture(here / "s1-mono-12.mp3", 15.0, 9.0, 12.0, +7.0 ),
Fixture(here / "s1-stereo-12.mp3", 15.0, 1.2, 15.0, +6.1 ), Fixture(here / "s1-stereo-12.mp3", 15.0, 9.0, 12.0, +6.1 ),
Fixture(here / "s1-mono+12.mp3", 15.0, 1.2, 15.0, -17.0 ), Fixture(here / "s1-mono+12.mp3", 15.0, 3.5, 13.0, -17.0 ),
Fixture(here / "s1-stereo+12.mp3", 15.0, 1.2, 15.0, -17.8 ), Fixture(here / "s1-stereo+12.mp3", 15.0, 3.5, 13.0, -17.8 ),
Fixture(here / "s1-mono.flac", 15.0, 1.4, 15.0, -2.3 ), Fixture(here / "s1-mono.flac", 15.0, 6.0, 13.0, -2.3 ),
Fixture(here / "s1-stereo.flac", 15.0, 1.4, 15.0, -6.0 ), Fixture(here / "s1-stereo.flac", 15.0, 6.0, 13.0, -6.0 ),
Fixture(here / "s1-mono-12.flac", 15.0, 2.0, 15.0, +10.0 ), Fixture(here / "s1-mono-12.flac", 15.0, 9.0, 12.0, +10.0 ),
Fixture(here / "s1-stereo-12.flac", 15.0, 1.8, 15.0, +5.9 ), Fixture(here / "s1-stereo-12.flac", 15.0, 9.0, 12.0, +5.9 ),
Fixture(here / "s1-mono+12.flac", 15.0, 0.0, 15.0, -12.0 ), Fixture(here / "s1-mono+12.flac", 15.0, 3.5, 13.0, -12.0 ),
Fixture(here / "s1-stereo+12.flac", 15.0, 0.0, 15.0, -14.9 ), Fixture(here / "s1-stereo+12.flac", 15.0, 3.5, 13.0, -14.9 ),
Fixture(here / "s1-mono.m4a", 15.0, 1.4, 15.0, -4.5 ), Fixture(here / "s1-mono.m4a", 15.0, 6.0, 13.0, -4.5 ),
Fixture(here / "s1-stereo.m4a", 15.0, 1.4, 15.0, -5.8 ), Fixture(here / "s1-stereo.m4a", 15.0, 6.0, 13.0, -5.8 ),
Fixture(here / "s1-mono.ogg", 15.0, 1.4, 15.0, -4.9 ), Fixture(here / "s1-mono.ogg", 15.0, 6.0, 13.0, -4.9 ),
Fixture(here / "s1-stereo.ogg", 15.0, 1.4, 15.0, -5.7 ), Fixture(here / "s1-stereo.ogg", 15.0, 6.0, 13.0, -5.7 ),
Fixture(here / "s1-stereo", 15.0, 1.4, 15.0, -5.7 ), Fixture(here / "s1-stereo", 15.0, 6.0, 13.0, -5.7 ),
Fixture(here / "s1-mono.wav", 15.0, 1.5, 15.0, -2.3 ), Fixture(here / "s1-mono.wav", 15.0, 6.0, 13.0, -2.3 ),
Fixture(here / "s1-stereo.wav", 15.0, 1.4, 15.0, -6.0 ), Fixture(here / "s1-stereo.wav", 15.0, 6.0, 13.0, -6.0 ),
# sample 1 large (looped for 2 hours) # sample 1 large (looped for 2 hours)
Fixture(here / "s1-large.flac", 7200, 1.4, 7200, -6.0 ), Fixture(here / "s1-large.flac", 7200, 6.0, 7198, -6.0 ),
# sample 2 # sample 2
# 0s -> 1.8s: silence # 0s -> 1.8s: silence
# 1.8s : noise # 1.8s : noise
@ -96,18 +96,12 @@ tags = {
"comment": "Test Comment", "comment": "Test Comment",
} }
mp3Tags = {
**tags,
"comments": tags["comment"],
"description": tags["comment"],
}
FILES_TAGGED = [ FILES_TAGGED = [
FixtureMeta( FixtureMeta(
here / "s1-jointstereo-tagged.mp3", here / "s1-jointstereo-tagged.mp3",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -117,7 +111,7 @@ FILES_TAGGED = [
here / "s1-mono-tagged.mp3", here / "s1-mono-tagged.mp3",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(64000, abs=1e2), "bit_rate": approx(64000, abs=1e2),
"channels": 1, "channels": 1,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -127,7 +121,7 @@ FILES_TAGGED = [
here / "s1-stereo-tagged.mp3", here / "s1-stereo-tagged.mp3",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -157,7 +151,7 @@ FILES_TAGGED = [
here / "s1-mono-tagged.m4a", here / "s1-mono-tagged.m4a",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(65000, abs=5e4), "bit_rate": approx(65000, abs=5e4),
"channels": 2, # Weird "channels": 2, # Weird
"mime": "audio/mp4", "mime": "audio/mp4",
@ -167,7 +161,7 @@ FILES_TAGGED = [
here / "s1-stereo-tagged.m4a", here / "s1-stereo-tagged.m4a",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(128000, abs=1e5), "bit_rate": approx(128000, abs=1e5),
"channels": 2, "channels": 2,
"mime": "audio/mp4", "mime": "audio/mp4",
@ -207,7 +201,7 @@ FILES_TAGGED = [
here / "s1-mono-tagged.wav", here / "s1-mono-tagged.wav",
{ {
**meta, **meta,
"bit_rate": approx(768000, abs=1e2), "bit_rate": approx(96000, abs=1e2),
"channels": 1, "channels": 1,
"mime": "audio/wav", "mime": "audio/wav",
}, },
@ -216,7 +210,7 @@ FILES_TAGGED = [
here / "s1-stereo-tagged.wav", here / "s1-stereo-tagged.wav",
{ {
**meta, **meta,
"bit_rate": approx(1536000, abs=1e2), "bit_rate": approx(384000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/wav", "mime": "audio/wav",
}, },
@ -234,18 +228,12 @@ tags = {
"comment": "Ł Ą Ż Ę Ć Ń Ś Ź", "comment": "Ł Ą Ż Ę Ć Ń Ś Ź",
} }
mp3Tags = {
**tags,
"comments": tags["comment"],
"description": tags["comment"],
}
FILES_TAGGED += [ FILES_TAGGED += [
FixtureMeta( FixtureMeta(
here / "s1-jointstereo-tagged-utf8.mp3", here / "s1-jointstereo-tagged-utf8.mp3",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -255,7 +243,7 @@ FILES_TAGGED += [
here / "s1-mono-tagged-utf8.mp3", here / "s1-mono-tagged-utf8.mp3",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(64000, abs=1e2), "bit_rate": approx(64000, abs=1e2),
"channels": 1, "channels": 1,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -265,7 +253,7 @@ FILES_TAGGED += [
here / "s1-stereo-tagged-utf8.mp3", here / "s1-stereo-tagged-utf8.mp3",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -295,7 +283,7 @@ FILES_TAGGED += [
here / "s1-mono-tagged-utf8.m4a", here / "s1-mono-tagged-utf8.m4a",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(65000, abs=5e4), "bit_rate": approx(65000, abs=5e4),
"channels": 2, # Weird "channels": 2, # Weird
"mime": "audio/mp4", "mime": "audio/mp4",
@ -305,7 +293,7 @@ FILES_TAGGED += [
here / "s1-stereo-tagged-utf8.m4a", here / "s1-stereo-tagged-utf8.m4a",
{ {
**meta, **meta,
**mp3Tags, **tags,
"bit_rate": approx(128000, abs=1e5), "bit_rate": approx(128000, abs=1e5),
"channels": 2, "channels": 2,
"mime": "audio/mp4", "mime": "audio/mp4",
@ -345,7 +333,7 @@ FILES_TAGGED += [
here / "s1-mono-tagged-utf8.wav", here / "s1-mono-tagged-utf8.wav",
{ {
**meta, **meta,
"bit_rate": approx(768000, abs=1e2), "bit_rate": approx(96000, abs=1e2),
"channels": 1, "channels": 1,
"mime": "audio/wav", "mime": "audio/wav",
}, },
@ -354,7 +342,7 @@ FILES_TAGGED += [
here / "s1-stereo-tagged-utf8.wav", here / "s1-stereo-tagged-utf8.wav",
{ {
**meta, **meta,
"bit_rate": approx(1536000, abs=1e2), "bit_rate": approx(384000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/wav", "mime": "audio/wav",
}, },

View File

@ -1,9 +1,7 @@
import distro
import pytest import pytest
from libretime_analyzer.pipeline.analyze_cuepoint import ( from libretime_analyzer.pipeline.analyze_cuepoint import analyze_cuepoint
analyze_cuepoint,
analyze_duration,
)
from ..fixtures import FILES from ..fixtures import FILES
@ -18,8 +16,11 @@ from ..fixtures import FILES
), ),
) )
def test_analyze_cuepoint(filepath, length, cuein, cueout): def test_analyze_cuepoint(filepath, length, cuein, cueout):
metadata = analyze_duration(filepath, {}) metadata = analyze_cuepoint(filepath, {})
metadata = analyze_cuepoint(filepath, metadata)
# On bionic, large file duration is a wrong.
if distro.codename() == "bionic" and str(filepath).endswith("s1-large.flac"):
return
assert metadata["length_seconds"] == pytest.approx(length, abs=0.1) assert metadata["length_seconds"] == pytest.approx(length, abs=0.1)
assert float(metadata["cuein"]) == pytest.approx(float(cuein), abs=1) assert float(metadata["cuein"]) == pytest.approx(float(cuein), abs=1)

View File

@ -27,8 +27,8 @@ def test_analyze_metadata(filepath: Path, metadata: dict):
del metadata["length"] del metadata["length"]
del found["length"] del found["length"]
# ogg,flac files does not support comments yet # mp3,ogg,flac files does not support comments yet
if not filepath.suffix == ".m4a" and not filepath.suffix == ".mp3": if not filepath.suffix == ".m4a":
if "comment" in metadata: if "comment" in metadata:
del metadata["comment"] del metadata["comment"]

View File

@ -33,8 +33,8 @@ def test_analyze_playability_invalid_filepath():
def test_analyze_playability_invalid_wma(): def test_analyze_playability_invalid_wma():
# Liquisoap does not fail with wma files on focal, bullseye, jammy # Liquisoap does not fail with wma files on buster, bullseye, focal, jammy
if distro.codename() in ("focal", "bullseye", "jammy"): if distro.codename() in ("buster", "bullseye", "focal", "jammy"):
return return
with pytest.raises(UnplayableFileError): with pytest.raises(UnplayableFileError):

View File

@ -1,3 +1,4 @@
import distro
import pytest import pytest
from libretime_analyzer.pipeline.analyze_replaygain import analyze_replaygain from libretime_analyzer.pipeline.analyze_replaygain import analyze_replaygain
@ -12,5 +13,10 @@ from ..fixtures import FILES
def test_analyze_replaygain(filepath, replaygain): def test_analyze_replaygain(filepath, replaygain):
tolerance = 0.8 tolerance = 0.8
# On bionic, replaygain is a bit higher for loud mp3 files.
# This huge tolerance makes the test pass, with values devianting from ~-17 to ~-13
if distro.codename() == "bionic" and str(filepath).endswith("+12.mp3"):
tolerance = 5
metadata = analyze_replaygain(filepath, {}) metadata = analyze_replaygain(filepath, {})
assert metadata["replay_gain"] == pytest.approx(replaygain, abs=tolerance) assert metadata["replay_gain"] == pytest.approx(replaygain, abs=tolerance)

View File

@ -1,3 +1,6 @@
from math import inf
import distro
import pytest import pytest
from libretime_analyzer.pipeline._ffmpeg import ( from libretime_analyzer.pipeline._ffmpeg import (
@ -27,6 +30,11 @@ def test_probe_replaygain(filepath, replaygain):
def test_compute_replaygain(filepath, replaygain): def test_compute_replaygain(filepath, replaygain):
tolerance = 0.8 tolerance = 0.8
# On bionic, replaygain is a bit higher for loud mp3 files.
# This huge tolerance makes the test pass, with values devianting from ~-17 to ~-13
if distro.codename() == "bionic" and str(filepath).endswith("+12.mp3"):
tolerance = 5
assert compute_replaygain(filepath) == pytest.approx(replaygain, abs=tolerance) assert compute_replaygain(filepath) == pytest.approx(replaygain, abs=tolerance)
@ -78,6 +86,10 @@ def test_silence_detect_re(line, expected):
def test_compute_silences(filepath, length, cuein, cueout): def test_compute_silences(filepath, length, cuein, cueout):
result = compute_silences(filepath) result = compute_silences(filepath)
# On bionic, large file duration is a wrong.
if distro.codename() == "bionic" and str(filepath).endswith("s1-large.flac"):
return
if cuein != 0.0: if cuein != 0.0:
assert len(result) > 0 assert len(result) > 0
first = result.pop(0) first = result.pop(0)
@ -85,6 +97,11 @@ def test_compute_silences(filepath, length, cuein, cueout):
assert first[1] == pytest.approx(cuein, abs=1) assert first[1] == pytest.approx(cuein, abs=1)
if cueout != length: if cueout != length:
# ffmpeg v3 (bionic) does not warn about silence end when the track ends.
# Check for infinity on last silence ending
if distro.codename() == "bionic":
length = inf
assert len(result) > 0 assert len(result) > 0
last = result.pop() last = result.pop()
assert last[0] == pytest.approx(cueout, abs=1) assert last[0] == pytest.approx(cueout, abs=1)
@ -96,4 +113,8 @@ def test_compute_silences(filepath, length, cuein, cueout):
map(lambda i: pytest.param(i.path, i.length, id=i.path.name), FILES), map(lambda i: pytest.param(i.path, i.length, id=i.path.name), FILES),
) )
def test_probe_duration(filepath, length): def test_probe_duration(filepath, length):
# On bionic, large file duration is a wrong.
if distro.codename() == "bionic" and str(filepath).endswith("s1-large.flac"):
return
assert probe_duration(filepath) == pytest.approx(length, abs=0.05) assert probe_duration(filepath) == pytest.approx(length, abs=0.05)

View File

@ -4,7 +4,7 @@ from queue import Queue
import pytest import pytest
from libretime_analyzer.pipeline import Pipeline, PipelineOptions from libretime_analyzer.pipeline import Pipeline
from ..conftest import AUDIO_FILENAME, AUDIO_IMPORT_DEST from ..conftest import AUDIO_FILENAME, AUDIO_IMPORT_DEST
@ -16,7 +16,6 @@ def test_run_analysis(src_dir: Path, dest_dir: Path):
str(src_dir / AUDIO_FILENAME), str(src_dir / AUDIO_FILENAME),
str(dest_dir), str(dest_dir),
AUDIO_FILENAME, AUDIO_FILENAME,
PipelineOptions(),
) )
metadata = queue.get() metadata = queue.get()

View File

@ -5,12 +5,12 @@ include ../tools/python.mk
PIP_INSTALL := \ PIP_INSTALL := \
--editable ../shared \ --editable ../shared \
--editable .[dev] --editable .[dev]
PYLINT_ARG := libretime_api_client tests PYLINT_ARG := libretime_api_client tests || true
MYPY_ARG := libretime_api_client tests MYPY_ARG := libretime_api_client tests || true
BANDIT_ARG := libretime_api_client BANDIT_ARG := libretime_api_client || true
PYTEST_ARG := --cov=libretime_api_client tests
format: .format format: .format
lint: .format-check .pylint .mypy .bandit lint: .format-check .pylint .mypy .bandit
test: .pytest test: .pytest
test-coverage: .coverage
clean: .clean clean: .clean

View File

@ -1,4 +0,0 @@
from importlib.metadata import version as get_version
PACKAGE = __name__
VERSION = get_version(__name__)

View File

@ -1,13 +1,11 @@
import logging
from typing import Optional from typing import Optional
from loguru import logger
from requests import Response, Session as BaseSession from requests import Response, Session as BaseSession
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException from requests.exceptions import RequestException
from urllib3.util import Retry from urllib3.util import Retry
logger = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 5 DEFAULT_TIMEOUT = 5
@ -26,26 +24,20 @@ class TimeoutHTTPAdapter(HTTPAdapter):
return super().send(request, *args, **kwargs) return super().send(request, *args, **kwargs)
def default_retry(max_retries: int = 5):
return Retry(
total=max_retries,
backoff_factor=2,
status_forcelist=[413, 429, 500, 502, 503, 504],
)
class Session(BaseSession): class Session(BaseSession):
base_url: Optional[str] base_url: Optional[str]
def __init__( def __init__(self, base_url: Optional[str] = None):
self,
base_url: Optional[str] = None,
retry: Optional[Retry] = None,
):
super().__init__() super().__init__()
self.base_url = base_url self.base_url = base_url
adapter = TimeoutHTTPAdapter(max_retries=retry) retry_strategy = Retry(
total=5,
backoff_factor=2,
status_forcelist=[413, 429, 500, 502, 503, 504],
)
adapter = TimeoutHTTPAdapter(max_retries=retry_strategy)
self.mount("http://", adapter) self.mount("http://", adapter)
self.mount("https://", adapter) self.mount("https://", adapter)
@ -67,16 +59,9 @@ class AbstractApiClient:
session: Session session: Session
base_url: str base_url: str
def __init__( def __init__(self, base_url: str):
self,
base_url: str,
retry: Optional[Retry] = None,
):
self.base_url = base_url self.base_url = base_url
self.session = Session( self.session = Session(base_url=base_url)
base_url=base_url,
retry=retry,
)
def _request( def _request(
self, self,

View File

@ -0,0 +1,167 @@
import logging
from time import sleep
import requests
from requests.auth import AuthBase
class UrlParamDict(dict):
def __missing__(self, key):
return "{" + key + "}"
class UrlException(Exception):
pass
class IncompleteUrl(UrlException):
def __init__(self, url):
super().__init__()
self.url = url
def __str__(self):
return f"Incomplete url: '{self.url}'"
class UrlBadParam(UrlException):
def __init__(self, url, param):
super().__init__()
self.url = url
self.param = param
def __str__(self):
return f"Bad param '{self.param}' passed into url: '{self.url}'"
# pylint: disable=too-few-public-methods
class KeyAuth(AuthBase):
def __init__(self, key):
self.key = key
def __call__(self, r):
r.headers["Authorization"] = f"Api-Key {self.key}"
return r
class ApcUrl:
"""A safe abstraction and testable for filling in parameters in
api_client.cfg"""
def __init__(self, base_url):
self.base_url = base_url
def params(self, **params):
temp_url = self.base_url
for k in params:
wrapped_param = "{" + k + "}"
if wrapped_param not in temp_url:
raise UrlBadParam(self.base_url, k)
temp_url = temp_url.format_map(UrlParamDict(**params))
return ApcUrl(temp_url)
def url(self):
if "{" in self.base_url:
raise IncompleteUrl(self.base_url)
return self.base_url
class ApiRequest:
API_HTTP_REQUEST_TIMEOUT = 30 # 30 second HTTP request timeout
def __init__(self, name, url, logger=None, api_key=None):
self.name = name
self.url = url
self.__req = None
if logger is None:
self.logger = logging
else:
self.logger = logger
self.auth = KeyAuth(api_key)
def __call__(self, *, _post_data=None, _put_data=None, params=None, **kwargs):
final_url = self.url.params(**kwargs).url()
self.logger.debug(final_url)
try:
if _post_data is not None:
res = requests.post(
final_url,
data=_post_data,
auth=self.auth,
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT,
)
elif _put_data is not None:
res = requests.put(
final_url,
data=_put_data,
auth=self.auth,
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT,
)
else:
res = requests.get(
final_url,
params=params,
auth=self.auth,
timeout=ApiRequest.API_HTTP_REQUEST_TIMEOUT,
)
# Check for bad HTTP status code
res.raise_for_status()
if "application/json" in res.headers["content-type"]:
return res.json()
return res
except requests.exceptions.Timeout:
self.logger.error("HTTP request to %s timed out", final_url)
raise
except requests.exceptions.HTTPError:
self.logger.error(
f"{res.request.method} {res.request.url} request failed '{res.status_code}':"
f"\nPayload: {res.request.body}"
f"\nResponse: {res.text}"
)
raise
def req(self, *args, **kwargs):
self.__req = lambda: self(*args, **kwargs)
return self
def retry(self, count, delay=5):
"""Try to send request n times. If after n times it fails then
we finally raise exception"""
for _ in range(0, count - 1):
try:
return self.__req()
except requests.exceptions.RequestException:
sleep(delay)
return self.__req()
class RequestProvider:
"""
Creates the available ApiRequest instance
"""
def __init__(self, base_url: str, api_key: str, endpoints: dict):
self.requests = {}
self.url = ApcUrl(base_url + "/{action}")
# Now we must discover the possible actions
for action_name, action_value in endpoints.items():
new_url = self.url.params(action=action_value)
if "{api_key}" in action_value:
new_url = new_url.params(api_key=api_key)
self.requests[action_name] = ApiRequest(
action_name, new_url, api_key=api_key
)
def available_requests(self):
return list(self.requests.keys())
def __contains__(self, request):
return request in self.requests
def __getattr__(self, attr):
if attr in self:
return self.requests[attr]
return super().__getattribute__(attr)

View File

@ -1,150 +1,124 @@
import json import json
import logging import logging
from functools import wraps import time
from time import sleep import urllib.parse
from requests.exceptions import RequestException import requests
from libretime_shared.config import BaseConfig, GeneralConfig
from ._client import AbstractApiClient, Response from ._utils import ApiRequest, RequestProvider
logger = logging.getLogger(__name__)
def retry_decorator(max_retries: int = 5): class Config(BaseConfig):
def retry_request(func): general: GeneralConfig
@wraps(func)
def wrapper(*args, **kwargs):
retries = max_retries
while True:
try:
return func(*args, **kwargs)
except RequestException as exception:
logger.warning(exception)
retries -= 1
if retries <= 0:
break
sleep(2.0)
return None
return wrapper
return retry_request
class BaseApiClient(AbstractApiClient): AIRTIME_API_VERSION = "1.1"
def __init__(self, base_url: str, api_key: str):
super().__init__(base_url=base_url)
self.session.headers.update({"Authorization": f"Api-Key {api_key}"})
self.session.params.update({"format": "json"}) # type: ignore[union-attr]
def version(self, **kwargs) -> Response:
return self._request(
"GET",
"/api/version",
**kwargs,
)
def register_component(self, component: str, **kwargs) -> Response: api_endpoints = {}
return self._request(
"GET",
"/api/register-component",
params={"component": component},
**kwargs,
)
def notify_media_item_start_play(self, media_id, **kwargs) -> Response: # URL to get the version number of the server API
return self._request( api_endpoints["version_url"] = "version/api_key/{api_key}"
"GET", # URL to register a components IP Address with the central web server
"/api/notify-media-item-start-play", api_endpoints[
params={"media_id": media_id}, "register_component"
**kwargs, ] = "register-component/format/json/api_key/{api_key}/component/{component}"
)
def update_liquidsoap_status(self, msg, stream_id, boot_time, **kwargs) -> Response: # media-monitor
return self._request( api_endpoints[
"POST", "upload_recorded"
"/api/update-liquidsoap-status", ] = "upload-recorded/format/json/api_key/{api_key}/fileid/{fileid}/showinstanceid/{showinstanceid}"
params={"stream_id": stream_id, "boot_time": boot_time}, # show-recorder
data={"msg_post": msg}, api_endpoints["show_schedule_url"] = "recorded-shows/format/json/api_key/{api_key}"
**kwargs, api_endpoints["upload_file_url"] = "rest/media"
) # pypo
api_endpoints[
def update_source_status(self, sourcename, status, **kwargs) -> Response: "update_start_playing_url"
return self._request( ] = "notify-media-item-start-play/api_key/{api_key}/media_id/{media_id}/"
"GET", api_endpoints[
"/api/update-source-status", "get_stream_setting"
params={"sourcename": sourcename, "status": status}, ] = "get-stream-setting/format/json/api_key/{api_key}/"
**kwargs, api_endpoints[
) "update_liquidsoap_status"
] = "update-liquidsoap-status/format/json/api_key/{api_key}/msg/{msg}/stream_id/{stream_id}/boot_time/{boot_time}"
def check_live_stream_auth(self, username, password, djtype, **kwargs) -> Response: api_endpoints[
return self._request( "update_source_status"
"GET", ] = "update-source-status/format/json/api_key/{api_key}/sourcename/{sourcename}/status/{status}"
"/api/check-live-stream-auth", api_endpoints[
params={"username": username, "password": password, "djtype": djtype}, "check_live_stream_auth"
**kwargs, ] = "check-live-stream-auth/format/json/api_key/{api_key}/username/{username}/password/{password}/djtype/{djtype}"
) api_endpoints["get_bootstrap_info"] = "get-bootstrap-info/format/json/api_key/{api_key}"
api_endpoints[
def notify_webstream_data(self, media_id, data, **kwargs) -> Response: "notify_webstream_data"
return self._request( ] = "notify-webstream-data/api_key/{api_key}/media_id/{media_id}/format/json"
"POST", api_endpoints[
"/api/notify-webstream-data", "notify_liquidsoap_started"
params={"media_id": media_id}, ] = "rabbitmq-do-push/api_key/{api_key}/format/json"
data={"data": data}, # Data is already a json formatted string api_endpoints[
**kwargs, "get_stream_parameters"
) ] = "get-stream-parameters/api_key/{api_key}/format/json"
api_endpoints["push_stream_stats"] = "push-stream-stats/api_key/{api_key}/format/json"
def rabbitmq_do_push(self, **kwargs) -> Response: api_endpoints[
return self._request( "update_stream_setting_table"
"GET", ] = "update-stream-setting-table/api_key/{api_key}/format/json"
"/api/rabbitmq-do-push", api_endpoints[
**kwargs, "update_metadata_on_tunein"
) ] = "update-metadata-on-tunein/api_key/{api_key}"
def push_stream_stats(self, data, **kwargs) -> Response:
return self._request(
"POST",
"/api/push-stream-stats",
data={"data": json.dumps(data)},
**kwargs,
)
def update_stream_setting_table(self, data, **kwargs) -> Response:
return self._request(
"POST",
"/api/update-stream-setting-table",
data={"data": json.dumps(data)},
**kwargs,
)
def update_metadata_on_tunein(self, **kwargs) -> Response:
return self._request(
"GET",
"/api/update-metadata-on-tunein",
**kwargs,
)
class ApiClient: class ApiClient:
def __init__(self, base_url: str, api_key: str): API_BASE = "/api"
self._base_client = BaseApiClient(base_url=base_url, api_key=api_key) UPLOAD_RETRIES = 3
UPLOAD_WAIT = 60
def version(self): def __init__(self, logger=None, config_path="/etc/libretime/config.yml"):
self.logger = logger or logging
config = Config(config_path)
self.base_url = config.general.public_url
self.api_key = config.general.api_key
self.services = RequestProvider(
base_url=self.base_url + self.API_BASE,
api_key=self.api_key,
endpoints=api_endpoints,
)
def __get_api_version(self):
try: try:
resp = self._base_client.version() return self.services.version_url()["api_version"]
payload = resp.json() except Exception as exception:
return payload["api_version"] self.logger.exception(exception)
except RequestException:
return -1 return -1
def is_server_compatible(self, verbose=True):
api_version = self.__get_api_version()
if api_version == -1:
if verbose:
self.logger.info("Unable to get Airtime API version number.\n")
return False
if api_version[0:3] != AIRTIME_API_VERSION[0:3]:
if verbose:
self.logger.info("Airtime API version found: " + str(api_version))
self.logger.info(
"pypo is only compatible with API version: " + AIRTIME_API_VERSION
)
return False
if verbose:
self.logger.info("Airtime API version found: " + str(api_version))
self.logger.info(
"pypo is only compatible with API version: " + AIRTIME_API_VERSION
)
return True
def notify_liquidsoap_started(self): def notify_liquidsoap_started(self):
try: try:
self._base_client.rabbitmq_do_push() self.services.notify_liquidsoap_started()
except RequestException: except Exception as exception:
pass self.logger.exception(exception)
def notify_media_item_start_playing(self, media_id): def notify_media_item_start_playing(self, media_id):
""" """
@ -153,20 +127,96 @@ class ApiClient:
which we handed to liquidsoap in get_liquidsoap_data(). which we handed to liquidsoap in get_liquidsoap_data().
""" """
try: try:
return self._base_client.notify_media_item_start_play(media_id=media_id) return self.services.update_start_playing_url(media_id=media_id)
except RequestException: except Exception as exception:
self.logger.exception(exception)
return None return None
def get_shows_to_record(self):
try:
return self.services.show_schedule_url()
except Exception as exception:
self.logger.exception(exception)
return None
def upload_recorded_show(self, files, show_id):
response = ""
retries = self.UPLOAD_RETRIES
retries_wait = self.UPLOAD_WAIT
url = self.construct_rest_url("upload_file_url")
self.logger.debug(url)
for i in range(0, retries):
self.logger.debug("Upload attempt: %s", i + 1)
self.logger.debug(files)
self.logger.debug(ApiRequest.API_HTTP_REQUEST_TIMEOUT)
try:
request = requests.post(
url, files=files, timeout=float(ApiRequest.API_HTTP_REQUEST_TIMEOUT)
)
response = request.json()
self.logger.debug(response)
# FIXME: We need to tell LibreTime that the uploaded track was recorded
# for a specific show
#
# My issue here is that response does not yet have an id. The id gets
# generated at the point where analyzer is done with it's work. We
# probably need to do what is below in analyzer and also make sure that
# the show instance id is routed all the way through.
#
# It already gets uploaded by this but the RestController does not seem
# to care about it. In the end analyzer doesn't have the info in it's
# rabbitmq message and imports the show as a regular track.
#
# logger.info("uploaded show result as file id %s", response.id)
#
# url = self.construct_url("upload_recorded") url =
# url.replace('%%fileid%%', response.id) url =
# url.replace('%%showinstanceid%%', show_id) request.get(url)
# logger.info("associated uploaded file %s with show instance %s",
# response.id, show_id)
break
except requests.exceptions.HTTPError as exception:
self.logger.error(f"Http error code: {exception.response.status_code}")
self.logger.exception(exception)
except requests.exceptions.ConnectionError as exception:
self.logger.exception(f"Server is down: {exception}")
except Exception as exception:
self.logger.exception(exception)
# wait some time before next retry
time.sleep(retries_wait)
return response
def check_live_stream_auth(self, username, password, dj_type): def check_live_stream_auth(self, username, password, dj_type):
try: try:
return self._base_client.check_live_stream_auth( return self.services.check_live_stream_auth(
username=username, username=username, password=password, djtype=dj_type
password=password,
djtype=dj_type,
) )
except RequestException: except Exception as exception:
self.logger.exception(exception)
return {} return {}
def construct_rest_url(self, action_key):
"""
Constructs the base url for RESTful requests
"""
url = urllib.parse.urlsplit(self.base_url)
url.username = self.api_key
return f"{url.geturl()}/{api_endpoints[action_key]}"
def get_stream_setting(self):
return self.services.get_stream_setting()
def register_component(self, component): def register_component(self, component):
""" """
Purpose of this method is to contact the server with a "Hey its Purpose of this method is to contact the server with a "Hey its
@ -175,45 +225,71 @@ class ApiClient:
to query monit via monit's http service, or download log files via a to query monit via monit's http service, or download log files via a
http server. http server.
""" """
return self._base_client.register_component(component=component) return self.services.register_component(component=component)
@retry_decorator()
def notify_liquidsoap_status(self, msg, stream_id, time): def notify_liquidsoap_status(self, msg, stream_id, time):
self._base_client.update_liquidsoap_status( try:
msg=msg, # encoded_msg is no longer used server_side!!
stream_id=stream_id, encoded_msg = urllib.parse.quote("dummy")
boot_time=time,
) self.services.update_liquidsoap_status.req(
_post_data={"msg_post": msg},
msg=encoded_msg,
stream_id=stream_id,
boot_time=time,
).retry(5)
except Exception as exception:
self.logger.exception(exception)
@retry_decorator()
def notify_source_status(self, sourcename, status): def notify_source_status(self, sourcename, status):
return self._base_client.update_source_status( try:
sourcename=sourcename, return self.services.update_source_status.req(
status=status, sourcename=sourcename, status=status
) ).retry(5)
except Exception as exception:
self.logger.exception(exception)
def get_bootstrap_info(self):
"""
Retrieve infomations needed on bootstrap time.
"""
return self.services.get_bootstrap_info()
@retry_decorator()
def notify_webstream_data(self, data, media_id): def notify_webstream_data(self, data, media_id):
""" """
Update the server with the latest metadata we've received from the Update the server with the latest metadata we've received from the
external webstream external webstream
""" """
return self._base_client.notify_webstream_data( self.logger.info(
data=data, self.services.notify_webstream_data.req(
media_id=str(media_id), _post_data={"data": data}, media_id=str(media_id)
).retry(5)
) )
def get_stream_parameters(self):
response = self.services.get_stream_parameters()
self.logger.debug(response)
return response
def push_stream_stats(self, data): def push_stream_stats(self, data):
return self._base_client.push_stream_stats(data=data) # TODO : users of this method should do their own error handling
response = self.services.push_stream_stats(
_post_data={"data": json.dumps(data)}
)
return response
def update_stream_setting_table(self, data): def update_stream_setting_table(self, data):
try: try:
return self._base_client.update_stream_setting_table(data=data) response = self.services.update_stream_setting_table(
except RequestException: _post_data={"data": json.dumps(data)}
return None )
return response
except Exception as exception:
self.logger.exception(exception)
def update_metadata_on_tunein(self): def update_metadata_on_tunein(self):
self._base_client.update_metadata_on_tunein() self.services.update_metadata_on_tunein()
def trigger_task_manager(self):
self._base_client.version() class InvalidContentType(Exception):
pass

View File

@ -1,14 +1,11 @@
from ._client import AbstractApiClient, Response, default_retry from ._client import AbstractApiClient, Response
class ApiClient(AbstractApiClient): class ApiClient(AbstractApiClient):
VERSION = "2.0" VERSION = "2.0"
def __init__(self, base_url: str, api_key: str): def __init__(self, base_url: str, api_key: str):
super().__init__( super().__init__(base_url=base_url)
base_url=base_url,
retry=default_retry(),
)
self.session.headers.update({"Authorization": f"Api-Key {api_key}"}) self.session.headers.update({"Authorization": f"Api-Key {api_key}"})
def get_info(self, **kwargs) -> Response: def get_info(self, **kwargs) -> Response:

View File

@ -1,8 +1,3 @@
[tool.isort]
profile = "black"
combine_as_imports = true
known_first_party = ["libretime_api_client"]
[tool.pylint.messages_control] [tool.pylint.messages_control]
extension-pkg-whitelist = "pydantic" extension-pkg-whitelist = "pydantic"
disable = [ disable = [
@ -11,13 +6,13 @@ disable = [
"missing-module-docstring", "missing-module-docstring",
] ]
[tool.pylint.format]
disable = "logging-fstring-interpolation"
[tool.pytest.ini_options] [tool.pytest.ini_options]
log_cli = true log_cli = true
log_cli_level = "DEBUG" log_cli_level = "DEBUG"
[tool.coverage.run]
source = ["libretime_api_client"]
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"

View File

@ -1,4 +1,4 @@
# Please do not edit this file, edit the setup.py file! # Please do not edit this file, edit the setup.py file!
# This file is auto-generated by tools/extract_requirements.py. # This file is auto-generated by tools/extract_requirements.py.
python-dateutil>=2.8.1,<2.10 python-dateutil>=2.8.1,<2.9
requests>=2.32.2,<2.33 requests>=2.25.1,<2.29

View File

@ -1,10 +1,8 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
version = "4.2.0" # x-release-please-version
setup( setup(
name="libretime-api-client", name="libretime-api-client",
version=version, version="3.0.0-beta.2",
description="LibreTime API Client", description="LibreTime API Client",
author="LibreTime Contributors", author="LibreTime Contributors",
url="https://github.com/libretime/libretime", url="https://github.com/libretime/libretime",
@ -16,16 +14,16 @@ setup(
license="AGPLv3", license="AGPLv3",
packages=find_packages(exclude=["*tests*", "*fixtures*"]), packages=find_packages(exclude=["*tests*", "*fixtures*"]),
package_data={"": ["py.typed"]}, package_data={"": ["py.typed"]},
python_requires=">=3.8", python_requires=">=3.6",
install_requires=[ install_requires=[
"python-dateutil>=2.8.1,<2.10", "python-dateutil>=2.8.1,<2.9",
"requests>=2.32.2,<2.33", "requests>=2.25.1,<2.29",
], ],
extras_require={ extras_require={
"dev": [ "dev": [
"requests-mock>=1.10.0,<2", "requests-mock",
"types-python-dateutil>=2.8.1,<3", "types-python-dateutil",
"types-requests>=2.31.0,<3", "types-requests",
], ],
}, },
zip_safe=False, zip_safe=False,

View File

@ -0,0 +1,95 @@
from unittest.mock import MagicMock, patch
import pytest
from libretime_api_client._utils import (
ApcUrl,
ApiRequest,
IncompleteUrl,
RequestProvider,
UrlBadParam,
)
@pytest.mark.parametrize(
"url, params, expected",
[
("one/two/three", {}, "one/two/three"),
("/testing/{key}", {"key": "aaa"}, "/testing/aaa"),
(
"/more/{key_a}/{key_b}/testing",
{"key_a": "aaa", "key_b": "bbb"},
"/more/aaa/bbb/testing",
),
],
)
def test_apc_url(url: str, params: dict, expected: str):
found = ApcUrl(url)
assert found.base_url == url
assert found.params(**params).url() == expected
def test_apc_url_bad_param():
url = ApcUrl("/testing/{key}")
with pytest.raises(UrlBadParam):
url.params(bad_key="testing")
def test_apc_url_incomplete():
url = ApcUrl("/{one}/{two}/three").params(two="testing")
with pytest.raises(IncompleteUrl):
url.url()
def test_api_request_init():
req = ApiRequest("request_name", ApcUrl("/test/ing"))
assert req.name == "request_name"
def test_api_request_call_json():
return_value = {"ok": "ok"}
read = MagicMock()
read.headers = {"content-type": "application/json"}
read.json = MagicMock(return_value=return_value)
with patch("requests.get") as mock_method:
mock_method.return_value = read
request = ApiRequest("mm", ApcUrl("http://localhost/testing"))()
assert request == return_value
def test_api_request_call_html():
return_value = "<html><head></head><body></body></html>"
read = MagicMock()
read.headers = {"content-type": "application/html"}
read.text = MagicMock(return_value=return_value)
with patch("requests.get") as mock_method:
mock_method.return_value = read
request = ApiRequest("mm", ApcUrl("http://localhost/testing"))()
assert request.text() == return_value
def test_request_provider_init():
request_provider = RequestProvider(
base_url="http://localhost/test",
api_key="test_key",
endpoints={},
)
assert len(request_provider.available_requests()) == 0
def test_request_provider_contains():
endpoints = {
"upload_recorded": "/1/",
}
request_provider = RequestProvider(
base_url="http://localhost/test",
api_key="test_key",
endpoints=endpoints,
)
for endpoint in endpoints:
assert endpoint in request_provider.requests

View File

@ -1,21 +0,0 @@
import pytest
from libretime_api_client.v1 import ApiClient
@pytest.mark.parametrize(
"base_url",
[
("http://localhost:8080"),
("http://localhost:8080/base"),
],
)
def test_api_client(requests_mock, base_url):
api_client = ApiClient(base_url=base_url, api_key="test-key")
requests_mock.get(
f"{base_url}/api/version",
json={"api_version": "1.0.0"},
)
assert api_client.version() == "1.0.0"

View File

@ -4,22 +4,32 @@ include ../tools/python.mk
PIP_INSTALL := \ PIP_INSTALL := \
--editable ../shared \ --editable ../shared \
--editable .[dev,sentry] --editable .[dev]
PYLINT_ARG := libretime_api PYLINT_ARG := libretime_api
MYPY_ARG := libretime_api MYPY_ARG := libretime_api
BANDIT_ARG := --exclude '*/tests/*' libretime_api || true BANDIT_ARG := --exclude '*/tests/*' libretime_api || true
export DJANGO_SETTINGS_MODULE=libretime_api.settings.testing
format: .format format: .format
lint: .format-check .pylint .mypy .bandit lint: .format-check .pylint .mypy .bandit
test: .pytest
test-coverage: .coverage
clean: .clean clean: .clean
test: $(VENV)
source $(VENV)/bin/activate
export DJANGO_SETTINGS_MODULE=libretime_api.settings.testing
pytest -v \
--numprocesses=$(CPU_CORES) \
--color=yes \
--cov-config=pyproject.toml \
--cov-report=term \
--cov-report=xml:./coverage.xml \
--cov=libretime_api \
libretime_api
SCHEMA_FILE ?= schema.yml SCHEMA_FILE ?= schema.yml
schema: $(VENV) schema: $(VENV)
$(VENV)/bin/libretime-api spectacular --file $(SCHEMA_FILE) source $(VENV)/bin/activate
export DJANGO_SETTINGS_MODULE=libretime_api.settings.testing
libretime-api spectacular --file $(SCHEMA_FILE)
if command -v npx > /dev/null; then npx prettier --write $(SCHEMA_FILE); fi if command -v npx > /dev/null; then npx prettier --write $(SCHEMA_FILE); fi
schema-foreach-commit: schema-foreach-commit:

View File

@ -11,6 +11,7 @@ PrivateTmp=true
PrivateUsers=true PrivateUsers=true
ProtectClock=true ProtectClock=true
ProtectControlGroups=true ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true ProtectHostname=true
ProtectKernelLogs=true ProtectKernelLogs=true
ProtectKernelModules=true ProtectKernelModules=true
@ -18,15 +19,14 @@ ProtectKernelTunables=true
ProtectProc=invisible ProtectProc=invisible
ProtectSystem=full ProtectSystem=full
Environment=PYTHONOPTIMIZE=2
Environment=LIBRETIME_CONFIG_FILEPATH=@@CONFIG_FILEPATH@@ Environment=LIBRETIME_CONFIG_FILEPATH=@@CONFIG_FILEPATH@@
Environment=LIBRETIME_LOG_FILEPATH=@@LOG_DIR@@/api.log Environment=LIBRETIME_LOG_FILEPATH=@@LOG_DIR@@/api.log
Type=notify Type=notify
KillMode=mixed KillMode=mixed
ExecStart=@@VENV_DIR@@/bin/gunicorn \ ExecStart=/usr/bin/gunicorn \
--workers 4 \ --workers 4 \
--worker-class libretime_api.gunicorn.Worker \ --worker-class uvicorn.workers.UvicornWorker \
--log-file - \ --log-file - \
--bind unix:/run/libretime-api.sock \ --bind unix:/run/libretime-api.sock \
libretime_api.asgi libretime_api.asgi

View File

@ -1,4 +0,0 @@
from importlib.metadata import version as get_version
PACKAGE = __name__
VERSION = get_version(__name__)

View File

@ -18,13 +18,12 @@ class StreamPreferences(BaseModel):
input_fade_transition: float input_fade_transition: float
message_format: MessageFormatKind message_format: MessageFormatKind
message_offline: str message_offline: str
replay_gain_enabled: bool
replay_gain_offset: float
# input_auto_switch_off: bool # input_auto_switch_off: bool
# input_auto_switch_on: bool # input_auto_switch_on: bool
# input_main_user: str # input_main_user: str
# input_main_password: str # input_main_password: str
# replay_gain_enabled: bool
# replay_gain_offset: float
# track_fade_in: float # track_fade_in: float
# track_fade_out: float # track_fade_out: float
# track_fade_transition: float # track_fade_transition: float
@ -79,12 +78,8 @@ class Preference(models.Model):
entries = dict(cls.site.values_list("key", "value")) entries = dict(cls.site.values_list("key", "value"))
return StreamPreferences( return StreamPreferences(
input_fade_transition=float(entries.get("default_transition_fade") or 0.0), input_fade_transition=float(entries.get("default_transition_fade") or 0.0),
message_format=MessageFormatKind( message_format=int(entries.get("stream_label_format") or 0),
int(entries.get("stream_label_format") or 0)
),
message_offline=entries.get("off_air_meta") or "Offline", message_offline=entries.get("off_air_meta") or "Offline",
replay_gain_enabled=entries.get("enable_replay_gain") == "1",
replay_gain_offset=float(entries.get("replay_gain_modifier") or 0.0),
) )
@classmethod @classmethod

View File

@ -8,7 +8,6 @@ from .role import Role
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
# pylint: disable=too-many-positional-arguments
def create_user(self, role, username, password, email, first_name, last_name): def create_user(self, role, username, password, email, first_name, last_name):
user = self.model( user = self.model(
role=role, role=role,
@ -21,7 +20,6 @@ class UserManager(BaseUserManager):
user.save(using=self._db) user.save(using=self._db)
return user return user
# pylint: disable=too-many-positional-arguments
def create_superuser(self, username, password, email, first_name, last_name): def create_superuser(self, username, password, email, first_name, last_name):
return self.create_user( return self.create_user(
Role.ADMIN, Role.ADMIN,

View File

@ -6,8 +6,6 @@ class StreamPreferencesSerializer(serializers.Serializer):
input_fade_transition = serializers.FloatField(read_only=True) input_fade_transition = serializers.FloatField(read_only=True)
message_format = serializers.IntegerField(read_only=True) message_format = serializers.IntegerField(read_only=True)
message_offline = serializers.CharField(read_only=True) message_offline = serializers.CharField(read_only=True)
replay_gain_enabled = serializers.BooleanField(read_only=True)
replay_gain_offset = serializers.FloatField(read_only=True)
# pylint: disable=abstract-method # pylint: disable=abstract-method

View File

@ -4,7 +4,7 @@ from libretime_api.core.models.preference import Preference
# pylint: disable=invalid-name,unused-argument # pylint: disable=invalid-name,unused-argument
def test_preference_get_site_preferences(db): def test_preference_get_site_preferences(db):
result = Preference.get_site_preferences() result = Preference.get_site_preferences()
assert result.model_dump() == { assert result.dict() == {
"station_name": "LibreTime", "station_name": "LibreTime",
} }
@ -12,19 +12,17 @@ def test_preference_get_site_preferences(db):
# pylint: disable=invalid-name,unused-argument # pylint: disable=invalid-name,unused-argument
def test_preference_get_stream_preferences(db): def test_preference_get_stream_preferences(db):
result = Preference.get_stream_preferences() result = Preference.get_stream_preferences()
assert result.model_dump() == { assert result.dict() == {
"input_fade_transition": 0.0, "input_fade_transition": 0.0,
"message_format": 0, "message_format": 0,
"message_offline": "LibreTime - offline", "message_offline": "LibreTime - offline",
"replay_gain_enabled": True,
"replay_gain_offset": 0.0,
} }
# pylint: disable=invalid-name,unused-argument # pylint: disable=invalid-name,unused-argument
def test_preference_get_stream_state(db): def test_preference_get_stream_state(db):
result = Preference.get_stream_state() result = Preference.get_stream_state()
assert result.model_dump() == { assert result.dict() == {
"input_main_connected": False, "input_main_connected": False,
"input_main_streaming": False, "input_main_streaming": False,
"input_show_connected": False, "input_show_connected": False,

View File

@ -9,8 +9,6 @@ def test_stream_preferences_get(db, api_client: APIClient):
"input_fade_transition": 0.0, "input_fade_transition": 0.0,
"message_format": 0, "message_format": 0,
"message_offline": "LibreTime - offline", "message_offline": "LibreTime - offline",
"replay_gain_enabled": True,
"replay_gain_offset": 0.0,
} }

View File

@ -22,7 +22,7 @@ class InfoView(APIView):
def get(self, request): def get(self, request):
data = Preference.get_site_preferences() data = Preference.get_site_preferences()
return Response( return Response(
data.model_dump( data.dict(
include={ include={
"station_name", "station_name",
} }

View File

@ -14,13 +14,11 @@ class StreamPreferencesView(views.APIView):
def get(self, request): def get(self, request):
data = Preference.get_stream_preferences() data = Preference.get_stream_preferences()
return Response( return Response(
data.model_dump( data.dict(
include={ include={
"input_fade_transition", "input_fade_transition",
"message_format", "message_format",
"message_offline", "message_offline",
"replay_gain_enabled",
"replay_gain_offset",
} }
) )
) )
@ -34,7 +32,7 @@ class StreamStateView(views.APIView):
def get(self, request): def get(self, request):
data = Preference.get_stream_state() data = Preference.get_stream_state()
return Response( return Response(
data.model_dump( data.dict(
include={ include={
"input_main_connected", "input_main_connected",
"input_main_streaming", "input_main_streaming",

View File

@ -1,5 +0,0 @@
from uvicorn.workers import UvicornWorker # pylint: disable=import-error
class Worker(UvicornWorker):
CONFIG_KWARGS = {"lifespan": "off"}

View File

@ -8,6 +8,8 @@ UP = """
-- DELETE FROM cc_pref WHERE keystr = 'system_version'; -- DELETE FROM cc_pref WHERE keystr = 'system_version';
-- INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '2.5.5'); -- INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '2.5.5');
ALTER TABLE cc_show ADD COLUMN image_path varchar(255) DEFAULT '';
ALTER TABLE cc_show_instances ADD COLUMN description varchar(255) DEFAULT '';
""" """
DOWN = None DOWN = None

View File

@ -5,7 +5,7 @@ from django.db import migrations
from ._migrations import legacy_migration_factory from ._migrations import legacy_migration_factory
UP = """ UP = """
ALTER TABLE cc_files ADD COLUMN artwork VARCHAR(255); ALTER TABLE cc_files ADD COLUMN artwork TYPE character varying(255);
""" """
DOWN = None DOWN = None

View File

@ -4,18 +4,12 @@ from django.db import migrations
from ._migrations import legacy_migration_factory from ._migrations import legacy_migration_factory
# This migration is currently a placeholder for 3.0.0-alpha.9.1.
# Please do not remove it. There are currently no actions, but it
# needs to remain intact so it does not fail when called from the
# migrations script. Any future migrations that may apply to
# 3.0.0-alpha.9.1 will be added to this file.
UP = """ UP = """
ALTER TABLE cc_files ADD COLUMN artwork VARCHAR(4096);
""" """
DOWN = """ DOWN = """
ALTER TABLE cc_files DROP COLUMN IF EXISTS artwork;
""" """

View File

@ -1,33 +0,0 @@
# pylint: disable=invalid-name
from django.db import migrations
from ._migrations import legacy_migration_factory
UP = """
drop table if exists "cc_stream_setting" cascade;
"""
DOWN = """
create table "cc_stream_setting"
(
"keyname" varchar(64) not null,
"value" varchar(255),
"type" varchar(16) not null,
primary key ("keyname")
);
"""
class Migration(migrations.Migration):
dependencies = [
("legacy", "0040_bump_legacy_schema_version"),
]
operations = [
migrations.RunPython(
code=legacy_migration_factory(
target="41",
sql=UP,
)
)
]

View File

@ -1,46 +0,0 @@
# pylint: disable=invalid-name
from django.db import migrations
from ._migrations import legacy_migration_factory
UP = """
delete from cc_pref
where "keystr" in (
'default_icecast_password',
'default_stream_mount_point',
'live_dj_connection_url_override',
'live_dj_source_connection_url',
'master_dj_connection_url_override',
'master_dj_source_connection_url',
'max_bitrate',
'num_of_streams',
'stream_bitrate',
'stream_type'
);
"""
DOWN = """
insert into
cc_pref ("keystr", "valstr")
values
('default_stream_mount_point', 'main'),
('max_bitrate', '320'),
('num_of_streams', '3'),
('stream_bitrate', '24, 32, 48, 64, 96, 128, 160, 192, 224, 256, 320'),
('stream_type', 'ogg, mp3, opus, aac');
"""
class Migration(migrations.Migration):
dependencies = [
("legacy", "0041_drop_stream_setting_table"),
]
operations = [
migrations.RunPython(
code=legacy_migration_factory(
target="42",
sql=UP,
)
)
]

View File

@ -1,26 +0,0 @@
# pylint: disable=invalid-name
from django.db import migrations
from ._migrations import legacy_migration_factory
UP = """
delete from cc_pref
where "keystr" = 'allowed_cors_urls';
"""
DOWN = """"""
class Migration(migrations.Migration):
dependencies = [
("legacy", "0042_remove_stream_preferences"),
]
operations = [
migrations.RunPython(
code=legacy_migration_factory(
target="43",
sql=UP,
)
)
]

View File

@ -1,29 +0,0 @@
# pylint: disable=invalid-name
from django.db import migrations
from ._migrations import legacy_migration_factory
UP = """
alter table "cc_track_types" add column "analyze_cue_points" boolean default 'f' not null;
update "cc_track_types" set "analyze_cue_points" = 't';
"""
DOWN = """
alter table "cc_track_types" drop column if exists "analyze_cue_points";
"""
class Migration(migrations.Migration):
dependencies = [
("legacy", "0043_remove_cors_preference"),
]
operations = [
migrations.RunPython(
code=legacy_migration_factory(
target="44",
sql=UP,
)
)
]

View File

@ -1,34 +0,0 @@
# pylint: disable=invalid-name
from django.db import migrations
from ._migrations import legacy_migration_factory
UP = """
CREATE TABLE "sessions"
(
"id" CHAR(32) NOT NULL,
"modified" INTEGER,
"lifetime" INTEGER,
"data" TEXT,
PRIMARY KEY ("id")
);
"""
DOWN = """
DROP TABLE IF EXISTS "sessions" CASCADE;
"""
class Migration(migrations.Migration):
dependencies = [
("legacy", "0044_add_track_types_analyzer_options"),
]
operations = [
migrations.RunPython(
code=legacy_migration_factory(
target="45",
sql=UP,
)
)
]

View File

@ -1,37 +0,0 @@
# pylint: disable=invalid-name
from django.db import migrations
from ._migrations import legacy_migration_factory
UP = """
ALTER TABLE cc_show ADD COLUMN override_intro_playlist boolean default 'f' NOT NULL;
ALTER TABLE cc_show ADD COLUMN intro_playlist_id integer DEFAULT NULL;
ALTER TABLE cc_show ADD CONSTRAINT cc_playlist_intro_playlist_fkey FOREIGN KEY (intro_playlist_id) REFERENCES cc_playlist (id) ON DELETE SET NULL;
ALTER TABLE cc_show ADD COLUMN override_outro_playlist boolean default 'f' NOT NULL;
ALTER TABLE cc_show ADD COLUMN outro_playlist_id integer DEFAULT NULL;
ALTER TABLE cc_show ADD CONSTRAINT cc_playlist_outro_playlist_fkey FOREIGN KEY (outro_playlist_id) REFERENCES cc_playlist (id) ON DELETE SET NULL;
"""
DOWN = """
ALTER TABLE cc_show DROP COLUMN IF EXISTS override_intro_playlist;
ALTER TABLE cc_show DROP COLUMN IF EXISTS intro_playlist_id;
ALTER TABLE cc_show DROP CONSTRAINT IF EXISTS cc_playlist_intro_playlist_fkey;
ALTER TABLE cc_show DROP COLUMN IF EXISTS override_outro_playlist;
ALTER TABLE cc_show DROP COLUMN IF EXISTS outro_playlist_id;
ALTER TABLE cc_show DROP CONSTRAINT IF EXISTS cc_playlist_outro_playlist_fkey;
"""
class Migration(migrations.Migration):
dependencies = [
("legacy", "0045_add_sessions_table"),
]
operations = [
migrations.RunPython(
code=legacy_migration_factory(
target="46",
sql=UP,
)
)
]

View File

@ -1,2 +1 @@
# The schema version is defined using the migration file prefix number LEGACY_SCHEMA_VERSION = "3.0.0-beta.0.1"
LEGACY_SCHEMA_VERSION = "46"

Some files were not shown because too many files have changed in this diff Show More