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
# Names
conet
falso
flor
# TODO: See https://github.com/savonet/liquidsoap/issues/1654

View File

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

View File

@ -5,6 +5,6 @@ contact_links:
url: https://discourse.libretime.org/
about: Please find existing questions and discussions here.
- name: LibreTime Chat (#libretime:matrix.org)
url: https://matrix.to/#/#libretime:matrix.org
- name: LibreTime Chat
url: https://chat.libretime.org/
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",
"commitMessageExtra": "({{packageFile}})",
"branchTopic": "lock-file-maintenance-{{packageFile}}",
"schedule": ["after 4am and before 5am on monday"],
"automerge": true,
"automergeType": "branch"
"schedule": ["after 4am and before 5am on monday"]
},
"baseBranches": ["main"],
"labels": ["dependencies"],
"packageRules": [
{
@ -27,9 +24,17 @@
"rangeStrategy": "widen"
},
{
"matchManagers": ["github-actions", "pre-commit"],
"automerge": true,
"automergeType": "branch"
"matchPaths": ["website/**"],
"addLabels": ["javascript"]
},
{
"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
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
@ -51,8 +51,10 @@ jobs:
fail-fast: false
matrix:
release:
- focal
- buster
- bullseye
- bionic
- focal
- jammy
container:
@ -63,9 +65,9 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ matrix.release }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
@ -73,11 +75,11 @@ jobs:
${{ matrix.release }}-pip-${{ inputs.context }}
- name: Test
run: make test-coverage
run: make test
working-directory: ${{ inputs.context }}
- name: Report coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v3
with:
files: ${{ inputs.context }}/coverage.xml
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
on:
workflow_dispatch:
push:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/_python.yml
- .github/workflows/analyzer.yml
- analyzer/**
- shared/**
- tools/python*
pull_request:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/_python.yml
- .github/workflows/analyzer.yml
- analyzer/**
- shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs:
python:

View File

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

View File

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

View File

@ -1,27 +1,21 @@
name: API
on:
workflow_dispatch:
push:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/_python.yml
- .github/workflows/api.yml
- api/**
- shared/**
- tools/python*
pull_request:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/_python.yml
- .github/workflows/api.yml
- api/**
- shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs:
python:
@ -36,8 +30,10 @@ jobs:
fail-fast: false
matrix:
release:
- focal
- buster
- bullseye
- bionic
- focal
- jammy
services:
@ -62,9 +58,9 @@ jobs:
LIBRETIME_DATABASE_HOST: postgres
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ matrix.release }}-pip-api-${{ hashFiles('api/**/setup.py') }}
@ -72,11 +68,11 @@ jobs:
${{ matrix.release }}-pip-api
- name: Test
run: make test-coverage
run: make test
working-directory: api
- name: Report coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v3
with:
files: api/coverage.xml
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:
push:
tags: ["[0-9]+.[0-9]+.[0-9]+*"]
branches: [main, stable-*]
branches: [main]
pull_request:
branches: [main, stable-*]
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
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:
needs: [meta]
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
NAMESPACE: ${{ github.repository_owner }}
if: ${{ github.repository_owner == 'libretime' }}
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
if: github.event_name == 'push'
uses: docker/login-action@v3
- uses: docker/login-action@v2
with:
registry: ghcr.io
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
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
run: |
make VERSION
echo "LIBRETIME_VERSION=$(cat VERSION | tr -d [:blank:])" >> $GITHUB_ENV
- name: Build
uses: docker/bake-action@v5
- name: Build python-builder
uses: docker/build-push-action@v3
with:
context: .
pull: true
push: ${{ github.event_name == 'push' }}
files: |
docker-bake.json
meta-analyzer/docker-metadata-action-bake.json
meta-api/docker-metadata-action-bake.json
meta-legacy/docker-metadata-action-bake.json
meta-playout/docker-metadata-action-bake.json
meta-worker/docker-metadata-action-bake.json
set: |
*.cache-from=type=gha,scope=container
*.cache-to=type=gha,scope=container,mode=max
*.args.LIBRETIME_VERSION=${{ env.LIBRETIME_VERSION }}
target: python-builder
cache-from: type=gha,scope=python-builder
cache-to: type=gha,scope=python-builder,mode=max
- name: Build python-base
uses: docker/build-push-action@v3
with:
context: .
pull: true
target: python-base
cache-from: type=gha,scope=python-base
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:
matrix:
include:
- distribution: ubuntu
release: bionic
- distribution: ubuntu
release: focal
- distribution: debian
release: bullseye
- distribution: ubuntu
release: jammy
- distribution: debian
release: buster
- distribution: debian
release: bullseye
- distribution: debian
release: bookworm
@ -30,10 +34,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Login to the Container registry
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@ -55,7 +59,7 @@ jobs:
COPY packages.list packages.list
EOF
[[ "${{ matrix.release }}" == "focal" ]] && \
[[ "${{ matrix.release }}" =~ "bionic|focal" ]] && \
cat <<EOF >> Dockerfile
RUN DEBIAN_FRONTEND=noninteractive apt-get --quiet update && \
DEBIAN_FRONTEND=noninteractive apt-get --quiet install -y software-properties-common && \
@ -78,7 +82,7 @@ jobs:
EOF
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.repository_owner == 'libretime' }}

View File

@ -2,21 +2,20 @@ name: Docs
on:
push:
branches: [main, stable-*]
branches: [main]
paths:
- .github/vale/**
- .github/workflows/docs.yml
- docs/**
- website/**
pull_request:
branches: [main, stable-*]
branches: [main]
paths:
- .github/vale/**
- .github/workflows/docs.yml
- docs/**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
- website/**
jobs:
lint:
@ -24,9 +23,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: |
/usr/local/bin/vale*
@ -43,7 +42,6 @@ jobs:
errata-ai/vale \
vale_{version}_Linux_64-bit.tar.gz --extract vale \
/usr/local/bin/vale \
--version v2.21.3 \
--version-file '{destination}.version'
- name: Add annotations matchers
@ -53,26 +51,5 @@ jobs:
- name: Run Vale
run: |
vale sync
vale --output line docs || true
vale --output line docs website/src/pages || true
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
default: "5"
permissions:
issues: write
jobs:
find_closed_references:
if: github.repository_owner == 'libretime'
@ -20,9 +17,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: "16"
@ -30,7 +27,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
issueLimit: ${{ github.event.inputs.issueLimit || '5' }}
ignore: .git,/docs/releases/*,CHANGELOG.md
ignore: .git,/docs/releases/*,/website/versioned*,CHANGELOG.md
find_broken_links:
if: github.repository_owner == 'libretime'
@ -38,9 +35,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: .lycheecache
key: housekeeping-find-broken-links-${{ github.sha }}
@ -48,59 +45,23 @@ jobs:
- name: Check Links
id: lychee
uses: lycheeverse/lychee-action@v2.1.0
uses: lycheeverse/lychee-action@v1.5.1
with:
args: >-
'**/*.md'
--exclude-path 'website/versioned_docs'
--require-https
--exclude-all-private
--exclude-mail
--exclude 'example\.(com|org)'
--exclude '\$server_name\$request_uri'
--exclude '%7Bvars.version%7D'
--exclude 'https://dir.xiph.org/cgi-bin/yp-cgi'
--exclude 'https://radio.indymedia.org/cgi-bin/yp-cgi'
--exclude 'https://www.ascap.com'
--exclude 'https://www.youtube-nocookie.com'
--exclude 'github\.com/libretime/libretime/(issues|pulls)'
--exclude 'https://packages.ubuntu.com/bionic/php7.2'
--exclude 'https://packages.ubuntu.com/bionic/python3'
--cache
--max-cache-age 2d
fail: true
env:
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
on:
workflow_dispatch:
push:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/legacy.yml
- api/**
- legacy/**
pull_request:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/legacy.yml
- api/**
- legacy/**
schedule:
- cron: 0 1 * * 1
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@ -30,10 +26,12 @@ jobs:
fail-fast: false
matrix:
include:
- php-version: "7.4" # Focal, Bullseye
- php-version: "7.2" # Bionic
- php-version: "7.3" # Buster
- php-version: "7.4" # Bullseye, Focal
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
@ -48,12 +46,14 @@ jobs:
fail-fast: false
matrix:
include:
- php-version: "7.4" # Focal, Bullseye
- php-version: "7.2" # Bionic
- php-version: "7.3" # Buster
- php-version: "7.4" # Bullseye, Focal
env:
ENVIRONMENT: testing
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Setup PostgreSQL
run: |
@ -62,7 +62,6 @@ jobs:
sudo -u postgres psql -c 'CREATE DATABASE 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 'ALTER DATABASE libretime OWNER TO libretime;'
sudo -u postgres psql -c 'ALTER USER libretime CREATEDB;'
- name: Setup PHP
@ -73,9 +72,9 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
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:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@ -85,36 +84,3 @@ jobs:
- name: Run tests
run: make test
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
on:
workflow_dispatch:
push:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/_python.yml
- .github/workflows/playout.yml
- playout/**
- api-client/**
- shared/**
- tools/python*
pull_request:
branches: [main, stable-*]
branches: [main]
paths:
- .github/workflows/_python.yml
- .github/workflows/playout.yml
- playout/**
- api-client/**
- shared/**
- tools/python*
schedule:
- cron: 0 1 * * 1
jobs:
python:

View File

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

View File

@ -1,12 +1,11 @@
name: Project
on:
workflow_dispatch:
push:
branches: [main, stable-*]
branches: [main]
pull_request:
types: [opened, reopened, synchronize, edited]
branches: [main, stable-*]
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -15,26 +14,67 @@ jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- uses: actions/cache@v4
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-project-pre-commit-pip-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
${{ 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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- 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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 7.4
@ -22,9 +22,10 @@ jobs:
- name: Build tarball
run: make tarball
- name: Upload tarball
uses: softprops/action-gh-release@v2
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
libretime-*.tar.gz
sha256sums.txt
body_path: docs/releases/${{ github.ref_name }}.md
draft: true
prerelease: true
files: libretime-*.tar.gz

View File

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

7
.gitignore vendored
View File

@ -8,13 +8,6 @@
*~
VERSION
/dev/certs/*
/dev/playout/*
/website/
!.gitkeep
## Github 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
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v4.3.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@ -26,63 +26,48 @@ repos:
exclude: \.ambr$
- id: name-tests-test
exclude: ^api
# TODO: Remove once the django api uses pytest
exclude: ^(api.*)$
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
rev: v2.7.1
hooks:
- id: prettier
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
rev: v3.19.0
rev: v2.38.2
hooks:
- id: pyupgrade
args: [--py38-plus]
args: [--py3-plus, --py36-plus]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.22.2
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
- repo: https://github.com/psf/black
rev: 22.8.0
hooks:
- 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
rev: v2.3.0
rev: v2.2.1
hooks:
- id: codespell
args: [--ignore-words=.codespellignore]
args:
- --ignore-words=.codespellignore
- --builtin=clear,rare,informal
exclude: (^api/schema.yml|^legacy.*|yarn\.lock)$
- repo: local
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
name: requirements.txt
description: Generate requirements.txt
entry: tools/extract_requirements.py dev sentry
entry: tools/extract_requirements.py dev
pass_filenames: false
language: script
files: setup.py$
@ -95,14 +80,6 @@ repos:
language: script
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
name: legacy-assets-checksum-update
description: Update legacy assets checksum
@ -110,11 +87,3 @@ repos:
pass_filenames: false
language: script
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
Packages = \
https://github.com/errata-ai/Google/releases/latest/download/Google.zip, \
https://github.com/errata-ai/Microsoft/releases/latest/download/Microsoft.zip
Vocab = Docs
[*.md]
BasedOnStyles = Vale, Microsoft, LibreTime
BasedOnStyles = Vale, Google, Microsoft, LibreTime
# Exclude emoji shortcodes `:tada:`
BlockIgnores = (:[a-z-_]+:)
Google.Units = False
Microsoft.GeneralURL = False
Microsoft.RangeFormat = False
Vale.Spelling = False

View File

@ -1,437 +1,4 @@
# Changelog
## [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
<a name="3.0.0-beta.1"></a>
## [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
- 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)
- [Release note](https://libretime.org/docs/releases/3.0.0-beta.0/)
@ -614,6 +183,8 @@
- improve containers build caching
- 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)
- [Release note](https://libretime.org/docs/releases/3.0.0-alpha.13/)
@ -774,6 +345,8 @@
- disable codecov project 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)
- [Release note](https://libretime.org/docs/releases/3.0.0-alpha.12/)
@ -788,6 +361,8 @@
- add missing data to release note
- 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)
- [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 #
#======================================================================================#
FROM python:3.10-slim-bullseye AS python-builder
FROM python:3.10-slim-bullseye as python-builder
WORKDIR /build
@ -18,7 +18,7 @@ RUN pip wheel --wheel-dir . --no-deps .
#======================================================================================#
# Python base #
#======================================================================================#
FROM python:3.10-slim-bullseye AS python-base
FROM python:3.10-slim-bullseye as python-base
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
@ -28,9 +28,11 @@ ARG USER=libretime
ARG UID=1000
ARG GID=1000
RUN set -eux \
&& adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER} \
&& install --directory --owner=${USER} /etc/libretime /srv/libretime
RUN adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER}
RUN install --directory --owner=${USER} \
/etc/libretime \
/srv/libretime
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 shared/packages.ini /tmp/packages.ini
RUN set -eux \
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
$(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /tmp/packages.py /tmp/packages.ini
@ -48,25 +49,23 @@ RUN set -eux \
#======================================================================================#
# Python base with ffmpeg #
#======================================================================================#
FROM python-base AS python-base-ffmpeg
FROM python-base as python-base-ffmpeg
RUN set -eux \
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
#======================================================================================#
# Analyzer #
#======================================================================================#
FROM python-base-ffmpeg AS libretime-analyzer
FROM python-base-ffmpeg as libretime-analyzer
COPY tools/packages.py /tmp/packages.py
COPY analyzer/packages.ini /tmp/packages.ini
RUN set -eux \
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
$(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /tmp/packages.py /tmp/packages.ini
@ -83,7 +82,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY analyzer .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[sentry]
pip install --editable .
# Run
USER ${UID}:${GID}
@ -97,14 +96,13 @@ ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
#======================================================================================#
# Playout #
#======================================================================================#
FROM python-base-ffmpeg AS libretime-playout
FROM python-base-ffmpeg as libretime-playout
COPY tools/packages.py /tmp/packages.py
COPY playout/packages.ini /tmp/packages.ini
RUN set -eux \
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
$(python3 /tmp/packages.py --format=line --exclude=python bullseye /tmp/packages.ini) \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /tmp/packages.py /tmp/packages.ini
@ -122,7 +120,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY playout .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[sentry]
pip install --editable .
# Run
USER ${UID}:${GID}
@ -136,12 +134,10 @@ ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
#======================================================================================#
# API #
#======================================================================================#
FROM python-base AS libretime-api
FROM python-base as libretime-api
RUN set -eux \
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl \
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gcc \
libc6-dev \
libpq-dev \
@ -151,7 +147,7 @@ WORKDIR /src
COPY api/requirements.txt .
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 .
RUN --mount=type=cache,target=/root/.cache/pip \
@ -159,7 +155,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY api .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[prod,sentry]
pip install --editable .[prod]
# Run
USER ${UID}:${GID}
@ -167,7 +163,7 @@ WORKDIR /app
CMD ["/usr/local/bin/gunicorn", \
"--workers=4", \
"--worker-class=libretime_api.gunicorn.Worker", \
"--worker-class=uvicorn.workers.UvicornWorker", \
"--log-file", "-", \
"--bind=0.0.0.0:9001", \
"libretime_api.asgi"]
@ -175,12 +171,10 @@ CMD ["/usr/local/bin/gunicorn", \
ARG LIBRETIME_VERSION
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
HEALTHCHECK CMD ["curl", "--fail", "http://localhost:9001/api/v2/version"]
#======================================================================================#
# Worker #
#======================================================================================#
FROM python-base AS libretime-worker
FROM python-base as libretime-worker
WORKDIR /src
@ -189,42 +183,47 @@ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt
COPY --from=python-builder /build/shared/*.whl .
COPY --from=python-builder /build/api-client/*.whl .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile *.whl && rm -Rf *.whl
COPY worker .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --editable .[sentry]
pip install --editable .
# Run
USER ${UID}:${GID}
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
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
#======================================================================================#
# 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_LOG_FILEPATH=php://stderr
# Custom user
ARG USER=libretime
ARG UID=1000
ARG GID=1000
RUN set -eux \
&& adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER} \
&& install --directory --owner=${USER} /etc/libretime /srv/libretime
RUN adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER}
RUN set -eux \
&& DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN install --directory --owner=${USER} \
/etc/libretime \
/srv/libretime
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
gettext \
libcurl4-openssl-dev \
libfreetype6-dev \
@ -269,9 +268,8 @@ COPY legacy/composer.* ./
RUN composer --no-cache install --no-progress --no-interaction --no-dev --no-autoloader
COPY legacy .
RUN set -eux \
&& make locale-build \
&& composer --no-cache dump-autoload --no-interaction --no-dev
RUN make locale-build
RUN composer --no-cache dump-autoload --no-interaction --no-dev
# Run
USER ${UID}:${GID}

View File

@ -7,57 +7,36 @@ all: setup
setup:
command -v pre-commit > /dev/null && pre-commit install
.env:
cp .env.dev .env
# https://google.github.io/styleguide/shellguide.html
shell-format:
shfmt -f . | xargs git ls-files | xargs shfmt -i 2 -ci -sr -kp -w
dev-certs:
rm -f dev/certs/fake.*
openssl req -x509 \
-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
shell-check:
shfmt -f . | xargs git ls-files | xargs shfmt -i 2 -ci -sr -kp -d
shfmt -f . | xargs git ls-files | xargs shellcheck --color=always --severity=$${SEVERITY:-style}
.PHONY: VERSION
VERSION:
tools/version.sh
changelog:
tools/changelog.sh
.PHONY: tarball
tarball: VERSION
$(MAKE) -C legacy build
cd .. && tar -czf libretime-$(shell cat VERSION | tr -d [:blank:]).tar.gz \
--owner=root --group=root \
--exclude-vcs \
libretime/analyzer \
libretime/api \
libretime/api-client \
libretime/docs \
libretime/installer \
libretime/legacy \
--exclude .codespellignore \
--exclude .git* \
--exclude .pre-commit-config.yaml \
--exclude dev_tools \
--exclude jekyll.sh \
--exclude legacy/vendor/phing \
--exclude legacy/vendor/simplepie/simplepie/tests \
libretime/playout \
libretime/shared \
libretime/tools \
libretime/worker \
libretime/CHANGELOG.md \
libretime/install \
libretime/LICENSE \
libretime/Makefile \
libretime/README.md \
libretime/SECURITY.md \
libretime/VERSION
libretime
mv ../libretime-*.tar.gz .
sha256sum libretime-*.tar.gz > sha256sums.txt
# Only clean subdirs
clean:
@ -65,13 +44,4 @@ clean:
docs-lint:
vale sync
vale docs
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
vale docs website/src/pages

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)
@ -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
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!
Please note that LibreTime is released with a [Contributor Code
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.
You can find details about our development process in the
[contributing](./CONTRIBUTING.md) guide.
Please submit enhancements, bug-fixes or comments via GitHub.
## 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
@ -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
the github issue queue for confirmed bugs and well-formed feature requests.
You can also contact us through [Matrix
(#libretime:matrix.org)](https://matrix.to/#/#libretime:matrix.org)
You can also contact us through our [Mattermost instance](https://chat.libretime.org)
where you can talk with other users and developers.
## 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
your organization. Your logo will show up here with a link to your website.
<a href="https://opencollective.com/libretime">
<img src="https://opencollective.com/libretime/organizations.svg?width=890">
<a href="https://opencollective.com/libretime/organization/0/website">
<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>
## License
@ -67,6 +99,6 @@ version 3 of the License.
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.

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_CPUS=4
# export VAGRANT_MEMORY=4096
# vagrant up bullseye
# vagrant up buster
#
Vagrant.configure('2') do |config|
@ -79,6 +79,7 @@ Vagrant.configure('2') do |config|
LIBRETIME_POSTGRESQL_PASSWORD=libretime \
LIBRETIME_RABBITMQ_PASSWORD=libretime \
bash install \
--listen-port 8080 \
--in-place \
http://192.168.10.100:8080
@ -98,6 +99,12 @@ Vagrant.configure('2') do |config|
setup_libretime(os, "debian.sh")
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|
os.vm.box = 'debian/bullseye64'
config.vm.provider 'virtualbox' do |v, override|
@ -106,4 +113,19 @@ Vagrant.configure('2') do |config|
setup_nfs(config, 4)
setup_libretime(os, 'debian.sh')
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

View File

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

View File

@ -10,6 +10,7 @@ PrivateTmp=true
PrivateUsers=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
@ -17,7 +18,6 @@ ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=full
Environment=PYTHONOPTIMIZE=2
Environment=LIBRETIME_CONFIG_FILEPATH=@@CONFIG_FILEPATH@@
Environment=LIBRETIME_LOG_FILEPATH=@@LOG_DIR@@/analyzer.log
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 typing import Optional
import click
from libretime_shared.cli import cli_config_options, cli_logging_options
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 .message_listener import MessageListener
from .status_reporter import StatusReporter
logger = logging.getLogger(__name__)
VERSION = "1.0"
DEFAULT_RETRY_QUEUE_FILEPATH = Path("retry_queue")
@ -38,19 +33,9 @@ def cli(
"""
Run analyzer.
"""
setup_logger(log_level, log_filepath)
setup_logger(level_from_name(log_level), log_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
StatusReporter.start_thread(retry_queue_filepath)

View File

@ -1,17 +1,15 @@
import json
import logging
import signal
import time
from queue import Queue
import pika
from loguru import logger
from .config import Config
from .pipeline import Pipeline, PipelineOptions, PipelineStatus
from .pipeline import Pipeline, PipelineStatus
from .status_reporter import StatusReporter
logger = logging.getLogger(__name__)
EXCHANGE = "airtime-uploads"
EXCHANGE_TYPE = "topic"
ROUTING_KEY = ""
@ -100,7 +98,7 @@ class MessageListener:
Here we parse the message, spin up an analyzer process, and report the
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 = ""
# final_file_path = ""
@ -113,19 +111,17 @@ class MessageListener:
body = body.decode()
except (UnicodeDecodeError, AttributeError):
pass
msg_dict: dict = json.loads(body)
msg_dict = json.loads(body)
file_id = msg_dict["file_id"]
audio_file_path = msg_dict["tmp_file_path"]
original_filename = msg_dict["original_filename"]
import_directory = msg_dict["import_directory"]
options = msg_dict.get("options", {})
metadata = MessageListener.spawn_analyzer_process(
audio_file_path,
import_directory,
original_filename,
options,
)
callback_url = f"{self.config.general.public_url}/rest/media/{file_id}"
@ -165,7 +161,6 @@ class MessageListener:
audio_file_path,
import_directory,
original_filename,
options: dict,
):
metadata = {}
@ -176,11 +171,10 @@ class MessageListener:
audio_file_path,
import_directory,
original_filename,
PipelineOptions(**options),
)
metadata = queue.get()
except Exception as exception:
logger.exception("Analyzer pipeline exception: %s", exception)
logger.exception(f"Analyzer pipeline exception: {exception}")
metadata["import_status"] = PipelineStatus.FAILED
# 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.
"""
cmd = _ffprobe("-i", filepath, errors="backslashreplace")
cmd = _ffprobe("-i", filepath)
track_gain_match = _PROBE_REPLAYGAIN_RE.search(cmd.stderr)
@ -75,7 +75,8 @@ def compute_silences(filepath: Path) -> List[Tuple[float, float]]:
cmd = _ffmpeg(
*("-i", filepath),
"-vn",
*("-filter", "highpass=frequency=80,silencedetect=noise=-60dB:duration=0.9"),
*("-filter", "highpass=frequency=1000"),
*("-filter", "silencedetect=noise=0.15:duration=1"),
)
starts, ends = [], []
@ -93,8 +94,9 @@ def compute_silences(filepath: Path) -> List[Tuple[float, float]]:
end = float(match.group(2))
ends.append(end)
# If one end is missing, set the last silence ending to infinity, and
# clamp it to the track duration before using this value.
# ffmpeg v3 (bionic) does not warn about silence end when the track ends.
# Set the last silence ending to infinity, and clamp it to the track duration before
# using this value.
if len(starts) - 1 == len(ends):
ends.append(inf)

View File

@ -1,7 +1,6 @@
import logging
from subprocess import CalledProcessError, CompletedProcess, run
from subprocess import PIPE, CalledProcessError, CompletedProcess, run
logger = logging.getLogger(__name__)
from loguru import logger
def run_(*args, **kwargs) -> CompletedProcess:
@ -9,14 +8,15 @@ def run_(*args, **kwargs) -> CompletedProcess:
return run(
args,
check=True,
capture_output=True,
text=True,
stdout=PIPE,
stderr=PIPE,
universal_newlines=True,
**kwargs,
)
except OSError as exception: # executable was not found
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
except CalledProcessError as exception: # returned an error code

View File

@ -1,18 +1,18 @@
import logging
from datetime import timedelta
from math import isclose
from subprocess import CalledProcessError
from typing import Any, Dict
from loguru import logger
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:
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["cuein"] = 0.0
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)
if len(silences) > 2:

View File

@ -1,26 +1,10 @@
import logging
from datetime import timedelta
from pathlib import Path
from typing import Any, Dict
import mutagen
from libretime_shared.files import compute_md5
from mutagen.easyid3 import EasyID3
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)
from loguru import logger
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
extracted = mutagen.File(filepath, easy=True)
if extracted is None:
logger.warning("no metadata were extracted for %s", filepath)
logger.warning(f"no metadata were extracted for {filepath}")
return metadata
metadata["mime"] = extracted.mime[0]
@ -85,36 +69,34 @@ def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
except (AttributeError, KeyError, IndexError):
pass
extracted_tags_mapping = [
("title", "track_title"),
("artist", "artist_name"),
("album", "album_title"),
("bpm", "bpm"),
("composer", "composer"),
("conductor", "conductor"),
("copyright", "copyright"),
("comment", "comment"),
("comment", "comments"),
("comment", "description"),
("encoded_by", "encoder"),
("genre", "genre"),
("isrc", "isrc"),
("label", "label"),
("organization", "label"),
# ("length", "length"),
("language", "language"),
("last_modified", "last_modified"),
("mood", "mood"),
("bit_rate", "bit_rate"),
("replay_gain", "replaygain"),
# ("tracknumber", "track_number"),
# ("track_total", "track_total"),
("website", "website"),
("date", "year"),
# ("mime_type", "mime"),
]
extracted_tags_mapping = {
"title": "track_title",
"artist": "artist_name",
"album": "album_title",
"bpm": "bpm",
"composer": "composer",
"conductor": "conductor",
"copyright": "copyright",
"comment": "comment",
"encoded_by": "encoder",
"genre": "genre",
"isrc": "isrc",
"label": "label",
"organization": "label",
# "length": "length",
"language": "language",
"last_modified": "last_modified",
"mood": "mood",
"bit_rate": "bit_rate",
"replay_gain": "replaygain",
# "tracknumber": "track_number",
# "track_total": "track_total",
"website": "website",
"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:
metadata[metadata_key] = extracted[extracted_key]
if isinstance(metadata[metadata_key], list):

View File

@ -1,10 +1,9 @@
import logging
from subprocess import CalledProcessError
from typing import Any, Dict
from ._liquidsoap import _liquidsoap
from loguru import logger
logger = logging.getLogger(__name__)
from ._liquidsoap import _liquidsoap
class UnplayableFileError(Exception):
@ -27,6 +26,6 @@ def analyze_playability(filename: str, metadata: Dict[str, Any]):
raise UnplayableFileError() from exception
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

View File

@ -1,9 +1,8 @@
import logging
import shutil
from pathlib import Path
from uuid import uuid4
logger = logging.getLogger(__name__)
from loguru import logger
MAX_DIR_LEN = 48
MAX_FILE_LEN = 48
@ -43,12 +42,12 @@ def organise_file(
return metadata
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
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)
metadata["full_path"] = str(dest_path)

View File

@ -1,22 +1,21 @@
import logging
from enum import Enum
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_playability import UnplayableFileError, analyze_playability
from .analyze_replaygain import analyze_replaygain
from .organise_file import organise_file
logger = logging.getLogger(__name__)
class Step(Protocol):
@staticmethod
def __call__(filename: str, metadata: Dict[str, Any]): ...
def __call__(filename: str, metadata: Dict[str, Any]):
...
class PipelineStatus(int, Enum):
@ -25,10 +24,6 @@ class PipelineStatus(int, Enum):
FAILED = 2
class PipelineOptions(BaseModel):
analyze_cue_points: bool = False
class Pipeline:
"""Analyzes and imports an audio file into the Airtime library.
@ -39,11 +34,10 @@ class Pipeline:
@staticmethod
def run_analysis(
queue: Queue,
audio_file_path: str,
import_directory: str,
original_filename: str,
options: PipelineOptions,
queue,
audio_file_path,
import_directory,
original_filename,
):
"""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:
metadata = {}
metadata = analyze_metadata(audio_file_path, metadata)
metadata = analyze_duration(audio_file_path, metadata)
if options.analyze_cue_points:
metadata = analyze_cuepoint(audio_file_path, metadata)
metadata = analyze_cuepoint(audio_file_path, metadata)
metadata = analyze_replaygain(audio_file_path, metadata)
metadata = analyze_playability(audio_file_path, metadata)

View File

@ -1,6 +1,5 @@
import collections
import json
import logging
import pickle
import queue
import threading
@ -8,10 +7,9 @@ import time
from urllib.parse import urlparse
import requests
from loguru import logger
from requests.exceptions import HTTPError
logger = logging.getLogger(__name__)
class PicklableHttpRequest:
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
# and continue because those HTTP requests are lost anyways. The pickled file will be
# 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:
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:
pickle.dump(retry_queue, pickle_file)
return
except (
Exception
) as exception: # Terrible top-level exception handler to prevent the thread from dying, just in case.
except Exception as exception: # Terrible top-level exception handler to prevent the thread from dying, just in case.
if shutdown:
return
logger.exception("Unhandled exception in StatusReporter %s", exception)
logger.exception(f"Unhandled exception in StatusReporter {exception}")
logger.info("Restarting StatusReporter thread")
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 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.
logger.exception("HTTP request failed: %s", exception)
logger.exception(f"HTTP request failed: {exception}")
parsed_url = urlparse(exception.response.request.url)
if is_web_server_broken(parsed_url.scheme + "://" + parsed_url.netloc):
# 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.
except requests.exceptions.ConnectionError as exception:
logger.exception(
"HTTP request failed due to a connection error, retrying later: %s",
exception,
f"HTTP request failed due to a connection error. Retrying later. {exception}"
)
retry_queue.append(picklable_request) # Retry it later
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.
# 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
@ -215,7 +210,7 @@ class StatusReporter:
audio_metadata["import_status"] = import_status
audio_metadata["comment"] = reason # hack attack
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(
PicklableHttpRequest(

View File

@ -1,16 +1,32 @@
# This file contains a list of package dependencies.
[python]
python3 = focal, bullseye, jammy, bookworm
python3-pip = focal, bullseye, jammy, bookworm
python3 = buster, bullseye, bookworm, bionic, focal, jammy
python3-pip = buster, bullseye, bookworm, bionic, focal, jammy
python3-pika = buster, bullseye, bookworm, bionic, focal, jammy
[liquidsoap]
# 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]
# Detect duration, silences and replaygain
ffmpeg = focal, bullseye, jammy, bookworm
ffmpeg = buster, bullseye, bookworm, bionic, focal, jammy
[=development]
# 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]
extension-pkg-whitelist = "pydantic"
disable = [
@ -11,13 +6,13 @@ disable = [
"missing-module-docstring",
]
[tool.pylint.format]
disable = "logging-fstring-interpolation"
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "DEBUG"
[tool.coverage.run]
source = ["libretime_analyzer"]
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

View File

@ -1,6 +1,6 @@
# Please do not edit this file, edit the setup.py file!
# 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
requests>=2.32.2,<2.33
requests>=2.25.1,<2.29
typing_extensions

View File

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

View File

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

View File

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

View File

@ -1,9 +1,7 @@
import distro
import pytest
from libretime_analyzer.pipeline.analyze_cuepoint import (
analyze_cuepoint,
analyze_duration,
)
from libretime_analyzer.pipeline.analyze_cuepoint import analyze_cuepoint
from ..fixtures import FILES
@ -18,8 +16,11 @@ from ..fixtures import FILES
),
)
def test_analyze_cuepoint(filepath, length, cuein, cueout):
metadata = analyze_duration(filepath, {})
metadata = analyze_cuepoint(filepath, metadata)
metadata = analyze_cuepoint(filepath, {})
# 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 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 found["length"]
# ogg,flac files does not support comments yet
if not filepath.suffix == ".m4a" and not filepath.suffix == ".mp3":
# mp3,ogg,flac files does not support comments yet
if not filepath.suffix == ".m4a":
if "comment" in metadata:
del metadata["comment"]

View File

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

View File

@ -1,3 +1,4 @@
import distro
import pytest
from libretime_analyzer.pipeline.analyze_replaygain import analyze_replaygain
@ -12,5 +13,10 @@ from ..fixtures import FILES
def test_analyze_replaygain(filepath, replaygain):
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, {})
assert metadata["replay_gain"] == pytest.approx(replaygain, abs=tolerance)

View File

@ -1,3 +1,6 @@
from math import inf
import distro
import pytest
from libretime_analyzer.pipeline._ffmpeg import (
@ -27,6 +30,11 @@ def test_probe_replaygain(filepath, replaygain):
def test_compute_replaygain(filepath, replaygain):
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)
@ -78,6 +86,10 @@ def test_silence_detect_re(line, expected):
def test_compute_silences(filepath, length, cuein, cueout):
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:
assert len(result) > 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)
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
last = result.pop()
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),
)
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)

View File

@ -4,7 +4,7 @@ from queue import Queue
import pytest
from libretime_analyzer.pipeline import Pipeline, PipelineOptions
from libretime_analyzer.pipeline import Pipeline
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(dest_dir),
AUDIO_FILENAME,
PipelineOptions(),
)
metadata = queue.get()

View File

@ -5,12 +5,12 @@ include ../tools/python.mk
PIP_INSTALL := \
--editable ../shared \
--editable .[dev]
PYLINT_ARG := libretime_api_client tests
MYPY_ARG := libretime_api_client tests
BANDIT_ARG := libretime_api_client
PYLINT_ARG := libretime_api_client tests || true
MYPY_ARG := libretime_api_client tests || true
BANDIT_ARG := libretime_api_client || true
PYTEST_ARG := --cov=libretime_api_client tests
format: .format
lint: .format-check .pylint .mypy .bandit
test: .pytest
test-coverage: .coverage
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 loguru import logger
from requests import Response, Session as BaseSession
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
from urllib3.util import Retry
logger = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 5
@ -26,26 +24,20 @@ class TimeoutHTTPAdapter(HTTPAdapter):
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):
base_url: Optional[str]
def __init__(
self,
base_url: Optional[str] = None,
retry: Optional[Retry] = None,
):
def __init__(self, base_url: Optional[str] = None):
super().__init__()
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("https://", adapter)
@ -67,16 +59,9 @@ class AbstractApiClient:
session: Session
base_url: str
def __init__(
self,
base_url: str,
retry: Optional[Retry] = None,
):
def __init__(self, base_url: str):
self.base_url = base_url
self.session = Session(
base_url=base_url,
retry=retry,
)
self.session = Session(base_url=base_url)
def _request(
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 logging
from functools import wraps
from time import sleep
import time
import urllib.parse
from requests.exceptions import RequestException
import requests
from libretime_shared.config import BaseConfig, GeneralConfig
from ._client import AbstractApiClient, Response
logger = logging.getLogger(__name__)
from ._utils import ApiRequest, RequestProvider
def retry_decorator(max_retries: int = 5):
def retry_request(func):
@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 Config(BaseConfig):
general: GeneralConfig
class BaseApiClient(AbstractApiClient):
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]
AIRTIME_API_VERSION = "1.1"
def version(self, **kwargs) -> Response:
return self._request(
"GET",
"/api/version",
**kwargs,
)
def register_component(self, component: str, **kwargs) -> Response:
return self._request(
"GET",
"/api/register-component",
params={"component": component},
**kwargs,
)
api_endpoints = {}
def notify_media_item_start_play(self, media_id, **kwargs) -> Response:
return self._request(
"GET",
"/api/notify-media-item-start-play",
params={"media_id": media_id},
**kwargs,
)
# URL to get the version number of the server API
api_endpoints["version_url"] = "version/api_key/{api_key}"
# URL to register a components IP Address with the central web server
api_endpoints[
"register_component"
] = "register-component/format/json/api_key/{api_key}/component/{component}"
def update_liquidsoap_status(self, msg, stream_id, boot_time, **kwargs) -> Response:
return self._request(
"POST",
"/api/update-liquidsoap-status",
params={"stream_id": stream_id, "boot_time": boot_time},
data={"msg_post": msg},
**kwargs,
)
def update_source_status(self, sourcename, status, **kwargs) -> Response:
return self._request(
"GET",
"/api/update-source-status",
params={"sourcename": sourcename, "status": status},
**kwargs,
)
def check_live_stream_auth(self, username, password, djtype, **kwargs) -> Response:
return self._request(
"GET",
"/api/check-live-stream-auth",
params={"username": username, "password": password, "djtype": djtype},
**kwargs,
)
def notify_webstream_data(self, media_id, data, **kwargs) -> Response:
return self._request(
"POST",
"/api/notify-webstream-data",
params={"media_id": media_id},
data={"data": data}, # Data is already a json formatted string
**kwargs,
)
def rabbitmq_do_push(self, **kwargs) -> Response:
return self._request(
"GET",
"/api/rabbitmq-do-push",
**kwargs,
)
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,
)
# media-monitor
api_endpoints[
"upload_recorded"
] = "upload-recorded/format/json/api_key/{api_key}/fileid/{fileid}/showinstanceid/{showinstanceid}"
# show-recorder
api_endpoints["show_schedule_url"] = "recorded-shows/format/json/api_key/{api_key}"
api_endpoints["upload_file_url"] = "rest/media"
# pypo
api_endpoints[
"update_start_playing_url"
] = "notify-media-item-start-play/api_key/{api_key}/media_id/{media_id}/"
api_endpoints[
"get_stream_setting"
] = "get-stream-setting/format/json/api_key/{api_key}/"
api_endpoints[
"update_liquidsoap_status"
] = "update-liquidsoap-status/format/json/api_key/{api_key}/msg/{msg}/stream_id/{stream_id}/boot_time/{boot_time}"
api_endpoints[
"update_source_status"
] = "update-source-status/format/json/api_key/{api_key}/sourcename/{sourcename}/status/{status}"
api_endpoints[
"check_live_stream_auth"
] = "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[
"notify_webstream_data"
] = "notify-webstream-data/api_key/{api_key}/media_id/{media_id}/format/json"
api_endpoints[
"notify_liquidsoap_started"
] = "rabbitmq-do-push/api_key/{api_key}/format/json"
api_endpoints[
"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"
api_endpoints[
"update_stream_setting_table"
] = "update-stream-setting-table/api_key/{api_key}/format/json"
api_endpoints[
"update_metadata_on_tunein"
] = "update-metadata-on-tunein/api_key/{api_key}"
class ApiClient:
def __init__(self, base_url: str, api_key: str):
self._base_client = BaseApiClient(base_url=base_url, api_key=api_key)
API_BASE = "/api"
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:
resp = self._base_client.version()
payload = resp.json()
return payload["api_version"]
except RequestException:
return self.services.version_url()["api_version"]
except Exception as exception:
self.logger.exception(exception)
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):
try:
self._base_client.rabbitmq_do_push()
except RequestException:
pass
self.services.notify_liquidsoap_started()
except Exception as exception:
self.logger.exception(exception)
def notify_media_item_start_playing(self, media_id):
"""
@ -153,20 +127,96 @@ class ApiClient:
which we handed to liquidsoap in get_liquidsoap_data().
"""
try:
return self._base_client.notify_media_item_start_play(media_id=media_id)
except RequestException:
return self.services.update_start_playing_url(media_id=media_id)
except Exception as exception:
self.logger.exception(exception)
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):
try:
return self._base_client.check_live_stream_auth(
username=username,
password=password,
djtype=dj_type,
return self.services.check_live_stream_auth(
username=username, password=password, djtype=dj_type
)
except RequestException:
except Exception as exception:
self.logger.exception(exception)
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):
"""
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
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):
self._base_client.update_liquidsoap_status(
msg=msg,
stream_id=stream_id,
boot_time=time,
)
try:
# encoded_msg is no longer used server_side!!
encoded_msg = urllib.parse.quote("dummy")
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):
return self._base_client.update_source_status(
sourcename=sourcename,
status=status,
)
try:
return self.services.update_source_status.req(
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):
"""
Update the server with the latest metadata we've received from the
external webstream
"""
return self._base_client.notify_webstream_data(
data=data,
media_id=str(media_id),
self.logger.info(
self.services.notify_webstream_data.req(
_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):
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):
try:
return self._base_client.update_stream_setting_table(data=data)
except RequestException:
return None
response = self.services.update_stream_setting_table(
_post_data={"data": json.dumps(data)}
)
return response
except Exception as exception:
self.logger.exception(exception)
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):
VERSION = "2.0"
def __init__(self, base_url: str, api_key: str):
super().__init__(
base_url=base_url,
retry=default_retry(),
)
super().__init__(base_url=base_url)
self.session.headers.update({"Authorization": f"Api-Key {api_key}"})
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]
extension-pkg-whitelist = "pydantic"
disable = [
@ -11,13 +6,13 @@ disable = [
"missing-module-docstring",
]
[tool.pylint.format]
disable = "logging-fstring-interpolation"
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "DEBUG"
[tool.coverage.run]
source = ["libretime_api_client"]
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

View File

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

View File

@ -1,10 +1,8 @@
from setuptools import find_packages, setup
version = "4.2.0" # x-release-please-version
setup(
name="libretime-api-client",
version=version,
version="3.0.0-beta.2",
description="LibreTime API Client",
author="LibreTime Contributors",
url="https://github.com/libretime/libretime",
@ -16,16 +14,16 @@ setup(
license="AGPLv3",
packages=find_packages(exclude=["*tests*", "*fixtures*"]),
package_data={"": ["py.typed"]},
python_requires=">=3.8",
python_requires=">=3.6",
install_requires=[
"python-dateutil>=2.8.1,<2.10",
"requests>=2.32.2,<2.33",
"python-dateutil>=2.8.1,<2.9",
"requests>=2.25.1,<2.29",
],
extras_require={
"dev": [
"requests-mock>=1.10.0,<2",
"types-python-dateutil>=2.8.1,<3",
"types-requests>=2.31.0,<3",
"requests-mock",
"types-python-dateutil",
"types-requests",
],
},
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 := \
--editable ../shared \
--editable .[dev,sentry]
--editable .[dev]
PYLINT_ARG := libretime_api
MYPY_ARG := libretime_api
BANDIT_ARG := --exclude '*/tests/*' libretime_api || true
export DJANGO_SETTINGS_MODULE=libretime_api.settings.testing
format: .format
lint: .format-check .pylint .mypy .bandit
test: .pytest
test-coverage: .coverage
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: $(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
schema-foreach-commit:

View File

@ -11,6 +11,7 @@ PrivateTmp=true
PrivateUsers=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
@ -18,15 +19,14 @@ ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=full
Environment=PYTHONOPTIMIZE=2
Environment=LIBRETIME_CONFIG_FILEPATH=@@CONFIG_FILEPATH@@
Environment=LIBRETIME_LOG_FILEPATH=@@LOG_DIR@@/api.log
Type=notify
KillMode=mixed
ExecStart=@@VENV_DIR@@/bin/gunicorn \
ExecStart=/usr/bin/gunicorn \
--workers 4 \
--worker-class libretime_api.gunicorn.Worker \
--worker-class uvicorn.workers.UvicornWorker \
--log-file - \
--bind unix:/run/libretime-api.sock \
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
message_format: MessageFormatKind
message_offline: str
replay_gain_enabled: bool
replay_gain_offset: float
# input_auto_switch_off: bool
# input_auto_switch_on: bool
# input_main_user: str
# input_main_password: str
# replay_gain_enabled: bool
# replay_gain_offset: float
# track_fade_in: float
# track_fade_out: float
# track_fade_transition: float
@ -79,12 +78,8 @@ class Preference(models.Model):
entries = dict(cls.site.values_list("key", "value"))
return StreamPreferences(
input_fade_transition=float(entries.get("default_transition_fade") or 0.0),
message_format=MessageFormatKind(
int(entries.get("stream_label_format") or 0)
),
message_format=int(entries.get("stream_label_format") or 0),
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

View File

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

View File

@ -6,8 +6,6 @@ class StreamPreferencesSerializer(serializers.Serializer):
input_fade_transition = serializers.FloatField(read_only=True)
message_format = serializers.IntegerField(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

View File

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

View File

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

View File

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

View File

@ -4,18 +4,12 @@ from django.db import migrations
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 = """
ALTER TABLE cc_files ADD COLUMN artwork VARCHAR(4096);
"""
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 = "46"
LEGACY_SCHEMA_VERSION = "3.0.0-beta.0.1"

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