Compare commits
196 Commits
Author | SHA1 | Date |
---|---|---|
|
5b4c720e10 | |
|
a14f1bec0b | |
|
203c927554 | |
|
6f5275176e | |
|
644d2b9ce5 | |
|
267da9e438 | |
|
b1bdd6d9be | |
|
92ca6b0341 | |
|
7f40743d83 | |
|
82d5af2dfb | |
|
cf172d5c7c | |
|
f1c9ebf6f2 | |
|
2985d8554a | |
|
f709c5026d | |
|
38a0bf98b2 | |
|
3be4bd7da1 | |
|
c0bb7df0ed | |
|
d09bf04379 | |
|
02a779b413 | |
|
ce257a1f35 | |
|
1939b0aec0 | |
|
2ac7e8a506 | |
|
824f6d2f1b | |
|
013d68e880 | |
|
83b56f9cd0 | |
|
c4e10ed861 | |
|
8d80e70580 | |
|
26db439d34 | |
|
380c6d0944 | |
|
7992a9be2d | |
|
2870857abc | |
|
0b221f4fff | |
|
188cd5d671 | |
|
74da2ef0b4 | |
|
08b85a44bc | |
|
2f0422b1ae | |
|
9ce88538d3 | |
|
299be3c142 | |
|
5b5c68c628 | |
|
16deaf08c6 | |
|
6d474c2733 | |
|
2202618150 | |
|
ba69de0a2b | |
|
32cad0faa4 | |
|
2643813fb7 | |
|
a37050df9e | |
|
bac903f4e5 | |
|
81e8fa90ed | |
|
004b784d09 | |
|
1ae9a7b368 | |
|
905008d72c | |
|
3b768644b2 | |
|
43f286c53d | |
|
a3865aa6ee | |
|
4d737319d8 | |
|
e614fbcf6c | |
|
d929871060 | |
|
e344154d42 | |
|
8c26505622 | |
|
c883d0f2d5 | |
|
f5355d6b61 | |
|
fdad983b48 | |
|
4b5ab6a2ad | |
|
72692c4b77 | |
|
1f9a504996 | |
|
65d435d9e7 | |
|
8136160125 | |
|
9ee1783afd | |
|
482f2215a0 | |
|
70735d4431 | |
|
e0b1eba1bb | |
|
e095cb2a5f | |
|
dad3d74188 | |
|
d97c4d427a | |
|
609b4e7a03 | |
|
e5aceef71a | |
|
63572fdab9 | |
|
60db15e8e5 | |
|
9684214425 | |
|
97b2f0e257 | |
|
646bc81724 | |
|
4e0953d513 | |
|
c96f78df9f | |
|
6958070eec | |
|
d99d6e1a68 | |
|
4642b6c08e | |
|
7f3f318601 | |
|
f12392e276 | |
|
e5a8baafa8 | |
|
451652bc40 | |
|
13a8e38beb | |
|
02cd85a845 | |
|
826aac1c05 | |
|
01253b31fc | |
|
f09bae6856 | |
|
5ed15f9722 | |
|
23c7411996 | |
|
2b43e51ed1 | |
|
10996e847b | |
|
6432efd791 | |
|
c3e30f1857 | |
|
9974b4631d | |
|
5d2ad43039 | |
|
848612ae6f | |
|
f77c8dbaaa | |
|
4442664c36 | |
|
1a985a4b37 | |
|
bfde17edf7 | |
|
9757b1b78c | |
|
86da46ee3a | |
|
201e85e537 | |
|
064c435b09 | |
|
a556b73d2a | |
|
ad16a9b3f8 | |
|
40b4fc7f66 | |
|
c02502ad9d | |
|
c5a9c75946 | |
|
50e5767963 | |
|
8615c85572 | |
|
7e92bc50cc | |
|
046aa724cc | |
|
da02e74f21 | |
|
71b20ae3c9 | |
|
a2cf7697a9 | |
|
a56edd9e98 | |
|
b721dbecd1 | |
|
7575b68472 | |
|
9c548b365e | |
|
7040d0e4bd | |
|
bcaa77ff3c | |
|
a63df8c989 | |
|
5ad69bf0b7 | |
|
f1c7dd89f1 | |
|
fb0584b021 | |
|
9192aaa2bb | |
|
0296446b70 | |
|
66436a7b9e | |
|
a5949ee155 | |
|
d31d4fc5bf | |
|
78f4fcfec8 | |
|
f6092e84d7 | |
|
634e6e236d | |
|
c286774e99 | |
|
e8e88f7d46 | |
|
f44445ae21 | |
|
d067d640dc | |
|
18a0eec000 | |
|
8ddd601b35 | |
|
d92168ac19 | |
|
cc28293150 | |
|
200dffef44 | |
|
feca75b28b | |
|
22c303cfff | |
|
9ef153fc6b | |
|
288a30d77b | |
|
6acf2e3f5a | |
|
8deebb3f85 | |
|
bace9ed917 | |
|
4584b52121 | |
|
270aa08ae6 | |
|
0e6e9ff7ff | |
|
64401150b1 | |
|
12461c4597 | |
|
117bc79809 | |
|
35d0dec4a8 | |
|
37d1a7685e | |
|
3b353ec74b | |
|
f7405f18ed | |
|
2b119adab2 | |
|
f4b260fdf7 | |
|
29f73e0dcb | |
|
3e05748d2d | |
|
ed2f874633 | |
|
7a3fffd45f | |
|
a95ce3d229 | |
|
170d09545e | |
|
199831458f | |
|
88a293370d | |
|
c2e6d15014 | |
|
12dd477312 | |
|
f6d7be9b1c | |
|
0c89350c2f | |
|
3d13f41bbd | |
|
dab6c486b8 | |
|
b14469722e | |
|
54ec07d2bd | |
|
0b5d63c547 | |
|
b6c3ece7d9 | |
|
c42a1c6660 | |
|
2a91320bb5 | |
|
5a581c034e | |
|
95283efc1f | |
|
f6d57d5f2d | |
|
4c40fab58e | |
|
02e258500b | |
|
ee33c0c56a |
|
@ -1 +1 @@
|
||||||
{".":"4.0.0"}
|
{".":"4.2.0"}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
"automergeType": "branch"
|
"automergeType": "branch"
|
||||||
},
|
},
|
||||||
"baseBranches": ["main", "stable"],
|
"baseBranches": ["main"],
|
||||||
"labels": ["dependencies"],
|
"labels": ["dependencies"],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,7 +29,7 @@ jobs:
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
|
key: ${{ runner.os }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
|
||||||
|
@ -65,7 +65,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ matrix.release }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
|
key: ${{ matrix.release }}-pip-${{ inputs.context }}-${{ hashFiles(format('{0}/{1}', inputs.context, '**/setup.py')) }}
|
||||||
|
@ -77,7 +77,7 @@ jobs:
|
||||||
working-directory: ${{ inputs.context }}
|
working-directory: ${{ inputs.context }}
|
||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
files: ${{ inputs.context }}/coverage.xml
|
files: ${{ inputs.context }}/coverage.xml
|
||||||
flags: ${{ inputs.context }}
|
flags: ${{ inputs.context }}
|
||||||
|
|
|
@ -3,7 +3,7 @@ name: Analyzer
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/analyzer.yml
|
- .github/workflows/analyzer.yml
|
||||||
|
@ -12,7 +12,7 @@ on:
|
||||||
- tools/python*
|
- tools/python*
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/analyzer.yml
|
- .github/workflows/analyzer.yml
|
||||||
|
|
|
@ -3,7 +3,7 @@ name: API Client
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/api-client.yml
|
- .github/workflows/api-client.yml
|
||||||
|
@ -12,7 +12,7 @@ on:
|
||||||
- tools/python*
|
- tools/python*
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/api-client.yml
|
- .github/workflows/api-client.yml
|
||||||
|
|
|
@ -29,7 +29,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-api-${{ hashFiles('api/**/setup.py') }}
|
key: ${{ runner.os }}-pip-api-${{ hashFiles('api/**/setup.py') }}
|
||||||
|
@ -42,7 +42,7 @@ jobs:
|
||||||
|
|
||||||
- name: Get pull request commit range
|
- name: Get pull request commit range
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
run: echo "COMMIT_RANGE=origin/${{ github.base_ref }}..${{ github.sha }}" >> $GITHUB_ENV
|
run: echo "COMMIT_RANGE=${{ github.sha }}~1...${{ github.sha }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Get push commit range
|
- name: Get push commit range
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|
|
@ -3,7 +3,7 @@ name: API
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/api.yml
|
- .github/workflows/api.yml
|
||||||
|
@ -12,7 +12,7 @@ on:
|
||||||
- tools/python*
|
- tools/python*
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/api.yml
|
- .github/workflows/api.yml
|
||||||
|
@ -64,7 +64,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ matrix.release }}-pip-api-${{ hashFiles('api/**/setup.py') }}
|
key: ${{ matrix.release }}-pip-api-${{ hashFiles('api/**/setup.py') }}
|
||||||
|
@ -76,7 +76,7 @@ jobs:
|
||||||
working-directory: api
|
working-directory: api
|
||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
files: api/coverage.xml
|
files: api/coverage.xml
|
||||||
flags: api
|
flags: api
|
||||||
|
|
|
@ -3,9 +3,9 @@ name: Container
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ["[0-9]+.[0-9]+.[0-9]+*"]
|
tags: ["[0-9]+.[0-9]+.[0-9]+*"]
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
@ -24,7 +24,7 @@ jobs:
|
||||||
|
|
||||||
- name: Update Docker Hub description
|
- name: Update Docker Hub description
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
uses: peter-evans/dockerhub-description@v3
|
uses: peter-evans/dockerhub-description@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
@ -86,7 +86,7 @@ jobs:
|
||||||
echo "LIBRETIME_VERSION=$(cat VERSION | tr -d [:blank:])" >> $GITHUB_ENV
|
echo "LIBRETIME_VERSION=$(cat VERSION | tr -d [:blank:])" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: docker/bake-action@v4
|
uses: docker/bake-action@v5
|
||||||
with:
|
with:
|
||||||
pull: true
|
pull: true
|
||||||
push: ${{ github.event_name == 'push' }}
|
push: ${{ github.event_name == 'push' }}
|
||||||
|
|
|
@ -78,7 +78,7 @@ jobs:
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.repository_owner == 'libretime' }}
|
push: ${{ github.repository_owner == 'libretime' }}
|
||||||
|
|
|
@ -2,14 +2,14 @@ name: Docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/vale/**
|
- .github/vale/**
|
||||||
- .github/workflows/docs.yml
|
- .github/workflows/docs.yml
|
||||||
- docs/**
|
- docs/**
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/vale/**
|
- .github/vale/**
|
||||||
- .github/workflows/docs.yml
|
- .github/workflows/docs.yml
|
||||||
|
@ -26,7 +26,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/usr/local/bin/vale*
|
/usr/local/bin/vale*
|
||||||
|
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .lycheecache
|
path: .lycheecache
|
||||||
key: housekeeping-find-broken-links-${{ github.sha }}
|
key: housekeeping-find-broken-links-${{ github.sha }}
|
||||||
|
@ -48,13 +48,12 @@ jobs:
|
||||||
|
|
||||||
- name: Check Links
|
- name: Check Links
|
||||||
id: lychee
|
id: lychee
|
||||||
uses: lycheeverse/lychee-action@v1.9.0
|
uses: lycheeverse/lychee-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
args: >-
|
args: >-
|
||||||
'**/*.md'
|
'**/*.md'
|
||||||
--require-https
|
--require-https
|
||||||
--exclude-all-private
|
--exclude-all-private
|
||||||
--exclude-mail
|
|
||||||
--exclude 'example\.(com|org)'
|
--exclude 'example\.(com|org)'
|
||||||
--exclude '\$server_name\$request_uri'
|
--exclude '\$server_name\$request_uri'
|
||||||
--exclude '%7Bvars.version%7D'
|
--exclude '%7Bvars.version%7D'
|
||||||
|
@ -63,6 +62,8 @@ jobs:
|
||||||
--exclude 'https://www.ascap.com'
|
--exclude 'https://www.ascap.com'
|
||||||
--exclude 'https://www.youtube-nocookie.com'
|
--exclude 'https://www.youtube-nocookie.com'
|
||||||
--exclude 'github\.com/libretime/libretime/(issues|pulls)'
|
--exclude 'github\.com/libretime/libretime/(issues|pulls)'
|
||||||
|
--exclude 'https://packages.ubuntu.com/bionic/php7.2'
|
||||||
|
--exclude 'https://packages.ubuntu.com/bionic/python3'
|
||||||
--cache
|
--cache
|
||||||
--max-cache-age 2d
|
--max-cache-age 2d
|
||||||
fail: true
|
fail: true
|
||||||
|
|
|
@ -3,14 +3,14 @@ name: Legacy
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/legacy.yml
|
- .github/workflows/legacy.yml
|
||||||
- api/**
|
- api/**
|
||||||
- legacy/**
|
- legacy/**
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/legacy.yml
|
- .github/workflows/legacy.yml
|
||||||
- api/**
|
- api/**
|
||||||
|
@ -62,6 +62,7 @@ jobs:
|
||||||
sudo -u postgres psql -c 'CREATE DATABASE libretime;'
|
sudo -u postgres psql -c 'CREATE DATABASE libretime;'
|
||||||
sudo -u postgres psql -c "CREATE USER libretime WITH PASSWORD 'libretime';"
|
sudo -u postgres psql -c "CREATE USER libretime WITH PASSWORD 'libretime';"
|
||||||
sudo -u postgres psql -c 'GRANT CONNECT ON DATABASE libretime TO libretime;'
|
sudo -u postgres psql -c 'GRANT CONNECT ON DATABASE libretime TO libretime;'
|
||||||
|
sudo -u postgres psql -c 'ALTER DATABASE libretime OWNER TO libretime;'
|
||||||
sudo -u postgres psql -c 'ALTER USER libretime CREATEDB;'
|
sudo -u postgres psql -c 'ALTER USER libretime CREATEDB;'
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
|
@ -74,7 +75,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.composer-cache.outputs.dir }}
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
|
|
@ -3,7 +3,7 @@ name: Playout
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/playout.yml
|
- .github/workflows/playout.yml
|
||||||
|
@ -13,7 +13,7 @@ on:
|
||||||
- tools/python*
|
- tools/python*
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/playout.yml
|
- .github/workflows/playout.yml
|
||||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5.4.0
|
- uses: amannn/action-semantic-pull-request@v5.5.3
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -3,10 +3,10 @@ name: Project
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, reopened, synchronize, edited]
|
types: [opened, reopened, synchronize, edited]
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
@ -21,14 +21,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-project-pre-commit-pip-${{ hashFiles('.pre-commit-config.yaml') }}
|
key: ${{ runner.os }}-project-pre-commit-pip-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-project-pre-commit-pip
|
${{ runner.os }}-project-pre-commit-pip
|
||||||
|
|
||||||
- uses: pre-commit/action@v3.0.0
|
- uses: pre-commit/action@v3.0.1
|
||||||
|
|
||||||
test-tools:
|
test-tools:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
||||||
run: make tarball
|
run: make tarball
|
||||||
|
|
||||||
- name: Upload tarball
|
- name: Upload tarball
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
libretime-*.tar.gz
|
libretime-*.tar.gz
|
||||||
|
|
|
@ -3,7 +3,7 @@ name: Shared
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/shared.yml
|
- .github/workflows/shared.yml
|
||||||
|
@ -11,7 +11,7 @@ on:
|
||||||
- tools/python*
|
- tools/python*
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/shared.yml
|
- .github/workflows/shared.yml
|
||||||
|
|
|
@ -3,7 +3,7 @@ name: Worker
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/worker.yml
|
- .github/workflows/worker.yml
|
||||||
|
@ -11,7 +11,7 @@ on:
|
||||||
- tools/python*
|
- tools/python*
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, stable]
|
branches: [main, stable-*]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/_python.yml
|
- .github/workflows/_python.yml
|
||||||
- .github/workflows/worker.yml
|
- .github/workflows/worker.yml
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
|
@ -36,13 +36,13 @@ repos:
|
||||||
exclude: ^(legacy/public(?!/js/airtime)|CHANGELOG.md$|.github/release-please-manifest.json)
|
exclude: ^(legacy/public(?!/js/airtime)|CHANGELOG.md$|.github/release-please-manifest.json)
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.15.0
|
rev: v3.19.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py38-plus]
|
args: [--py38-plus]
|
||||||
|
|
||||||
- repo: https://github.com/adamchainz/django-upgrade
|
- repo: https://github.com/adamchainz/django-upgrade
|
||||||
rev: 1.15.0
|
rev: 1.22.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: django-upgrade
|
- id: django-upgrade
|
||||||
args: [--target-version, "4.2"]
|
args: [--target-version, "4.2"]
|
||||||
|
@ -54,12 +54,12 @@ repos:
|
||||||
args: [--resolve-all-configs]
|
args: [--resolve-all-configs]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 23.12.1
|
rev: 24.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args: [--ignore-words=.codespellignore]
|
args: [--ignore-words=.codespellignore]
|
||||||
|
@ -110,3 +110,11 @@ repos:
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
files: ^legacy
|
files: ^legacy
|
||||||
|
|
||||||
|
- id: api-schema-update
|
||||||
|
name: api-schema-update
|
||||||
|
description: Ensure API schema is up to date
|
||||||
|
entry: make -C api schema
|
||||||
|
pass_filenames: false
|
||||||
|
language: system
|
||||||
|
files: ^api
|
||||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -1,5 +1,59 @@
|
||||||
# Changelog
|
# 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 <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 <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 <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 <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)
|
## [4.0.0](https://github.com/libretime/libretime/compare/3.2.0...4.0.0) (2024-01-07)
|
||||||
|
|
||||||
|
|
||||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -2,7 +2,7 @@ ARG LIBRETIME_VERSION
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# Python Builder #
|
# Python Builder #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM python:3.10-slim-bullseye as python-builder
|
FROM python:3.10-slim-bullseye AS python-builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ RUN pip wheel --wheel-dir . --no-deps .
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# Python base #
|
# Python base #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM python:3.10-slim-bullseye as python-base
|
FROM python:3.10-slim-bullseye AS python-base
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
@ -48,7 +48,7 @@ RUN set -eux \
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# Python base with ffmpeg #
|
# Python base with ffmpeg #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM python-base as python-base-ffmpeg
|
FROM python-base AS python-base-ffmpeg
|
||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get update \
|
&& DEBIAN_FRONTEND=noninteractive apt-get update \
|
||||||
|
@ -59,7 +59,7 @@ RUN set -eux \
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# Analyzer #
|
# Analyzer #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM python-base-ffmpeg as libretime-analyzer
|
FROM python-base-ffmpeg AS libretime-analyzer
|
||||||
|
|
||||||
COPY tools/packages.py /tmp/packages.py
|
COPY tools/packages.py /tmp/packages.py
|
||||||
COPY analyzer/packages.ini /tmp/packages.ini
|
COPY analyzer/packages.ini /tmp/packages.ini
|
||||||
|
@ -97,7 +97,7 @@ ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# Playout #
|
# Playout #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM python-base-ffmpeg as libretime-playout
|
FROM python-base-ffmpeg AS libretime-playout
|
||||||
|
|
||||||
COPY tools/packages.py /tmp/packages.py
|
COPY tools/packages.py /tmp/packages.py
|
||||||
COPY playout/packages.ini /tmp/packages.ini
|
COPY playout/packages.ini /tmp/packages.ini
|
||||||
|
@ -136,11 +136,12 @@ ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# API #
|
# API #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM python-base as libretime-api
|
FROM python-base AS libretime-api
|
||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get update \
|
&& DEBIAN_FRONTEND=noninteractive apt-get update \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
gcc \
|
gcc \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
|
@ -174,13 +175,12 @@ CMD ["/usr/local/bin/gunicorn", \
|
||||||
ARG LIBRETIME_VERSION
|
ARG LIBRETIME_VERSION
|
||||||
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
|
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
|
||||||
|
|
||||||
HEALTHCHECK CMD ["python3", "-c", \
|
HEALTHCHECK CMD ["curl", "--fail", "http://localhost:9001/api/v2/version"]
|
||||||
"import requests; requests.get('http://localhost:9001/api/v2/version').raise_for_status()"]
|
|
||||||
|
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# Worker #
|
# Worker #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM python-base as libretime-worker
|
FROM python-base AS libretime-worker
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
@ -189,6 +189,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
pip install --no-compile -r requirements.txt
|
pip install --no-compile -r requirements.txt
|
||||||
|
|
||||||
COPY --from=python-builder /build/shared/*.whl .
|
COPY --from=python-builder /build/shared/*.whl .
|
||||||
|
COPY --from=python-builder /build/api-client/*.whl .
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
pip install --no-compile *.whl && rm -Rf *.whl
|
pip install --no-compile *.whl && rm -Rf *.whl
|
||||||
|
|
||||||
|
@ -200,20 +201,14 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
USER ${UID}:${GID}
|
USER ${UID}:${GID}
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["/usr/local/bin/celery", "worker", \
|
CMD ["/usr/local/bin/libretime-worker"]
|
||||||
"--app=libretime_worker.tasks:worker", \
|
|
||||||
"--config=libretime_worker.config", \
|
|
||||||
"--time-limit=1800", \
|
|
||||||
"--concurrency=1", \
|
|
||||||
"--loglevel=info"]
|
|
||||||
|
|
||||||
ARG LIBRETIME_VERSION
|
ARG LIBRETIME_VERSION
|
||||||
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
|
ENV LIBRETIME_VERSION=$LIBRETIME_VERSION
|
||||||
|
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
# Legacy #
|
# Legacy #
|
||||||
#======================================================================================#
|
#======================================================================================#
|
||||||
FROM php:7.4-fpm as libretime-legacy
|
FROM php:7.4-fpm AS libretime-legacy
|
||||||
|
|
||||||
ENV LIBRETIME_CONFIG_FILEPATH=/etc/libretime/config.yml
|
ENV LIBRETIME_CONFIG_FILEPATH=/etc/libretime/config.yml
|
||||||
ENV LIBRETIME_LOG_FILEPATH=php://stderr
|
ENV LIBRETIME_LOG_FILEPATH=php://stderr
|
||||||
|
|
8
Makefile
8
Makefile
|
@ -22,10 +22,10 @@ dev-certs:
|
||||||
cat dev/certs/fake.{key,crt} > dev/certs/fake.pem
|
cat dev/certs/fake.{key,crt} > dev/certs/fake.pem
|
||||||
|
|
||||||
dev: .env dev-certs
|
dev: .env dev-certs
|
||||||
DOCKER_BUILDKIT=1 docker-compose build
|
DOCKER_BUILDKIT=1 docker compose build
|
||||||
docker-compose run --rm legacy make build
|
docker compose run --rm legacy make build
|
||||||
docker-compose run --rm api libretime-api migrate
|
docker compose run --rm api libretime-api migrate
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
.PHONY: VERSION
|
.PHONY: VERSION
|
||||||
VERSION:
|
VERSION:
|
||||||
|
|
19
README.md
19
README.md
|
@ -52,23 +52,8 @@ Become a financial contributor and help us sustain our community on
|
||||||
[Support](https://opencollective.com/libretime/contribute) this project with
|
[Support](https://opencollective.com/libretime/contribute) this project with
|
||||||
your organization. Your logo will show up here with a link to your website.
|
your organization. Your logo will show up here with a link to your website.
|
||||||
|
|
||||||
<a href="https://opencollective.com/libretime/organization/0/website">
|
<a href="https://opencollective.com/libretime">
|
||||||
<img src="https://opencollective.com/libretime/organization/0/avatar.svg">
|
<img src="https://opencollective.com/libretime/organizations.svg?width=890">
|
||||||
</a>
|
|
||||||
<a href="https://opencollective.com/libretime/organization/1/website">
|
|
||||||
<img src="https://opencollective.com/libretime/organization/1/avatar.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://opencollective.com/libretime/organization/2/website">
|
|
||||||
<img src="https://opencollective.com/libretime/organization/2/avatar.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://opencollective.com/libretime/organization/3/website">
|
|
||||||
<img src="https://opencollective.com/libretime/organization/3/avatar.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://opencollective.com/libretime/organization/4/website">
|
|
||||||
<img src="https://opencollective.com/libretime/organization/4/avatar.svg">
|
|
||||||
</a>
|
|
||||||
<a href="https://opencollective.com/libretime/organization/5/website">
|
|
||||||
<img src="https://opencollective.com/libretime/organization/5/avatar.svg">
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
@ -36,7 +36,7 @@ def probe_replaygain(filepath: Path) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Probe replaygain will probe the given audio file and return the replaygain if available.
|
Probe replaygain will probe the given audio file and return the replaygain if available.
|
||||||
"""
|
"""
|
||||||
cmd = _ffprobe("-i", filepath)
|
cmd = _ffprobe("-i", filepath, errors="backslashreplace")
|
||||||
|
|
||||||
track_gain_match = _PROBE_REPLAYGAIN_RE.search(cmd.stderr)
|
track_gain_match = _PROBE_REPLAYGAIN_RE.search(cmd.stderr)
|
||||||
|
|
||||||
|
@ -75,8 +75,7 @@ def compute_silences(filepath: Path) -> List[Tuple[float, float]]:
|
||||||
cmd = _ffmpeg(
|
cmd = _ffmpeg(
|
||||||
*("-i", filepath),
|
*("-i", filepath),
|
||||||
"-vn",
|
"-vn",
|
||||||
*("-filter", "highpass=frequency=1000"),
|
*("-filter", "highpass=frequency=80,silencedetect=noise=-60dB:duration=0.9"),
|
||||||
*("-filter", "silencedetect=noise=0.15:duration=1"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
starts, ends = [], []
|
starts, ends = [], []
|
||||||
|
|
|
@ -5,10 +5,24 @@ from typing import Any, Dict
|
||||||
|
|
||||||
import mutagen
|
import mutagen
|
||||||
from libretime_shared.files import compute_md5
|
from libretime_shared.files import compute_md5
|
||||||
|
from mutagen.easyid3 import EasyID3
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def flatten(xss):
|
||||||
|
return [x for xs in xss for x in xs]
|
||||||
|
|
||||||
|
|
||||||
|
def comment_get(id3, _):
|
||||||
|
comments = [v.text for k, v in id3.items() if "COMM" in k or "comment" in k]
|
||||||
|
|
||||||
|
return flatten(comments)
|
||||||
|
|
||||||
|
|
||||||
|
EasyID3.RegisterKey("comment", comment_get)
|
||||||
|
|
||||||
|
|
||||||
def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
|
def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
Extract audio metadata from tags embedded in the file using mutagen.
|
Extract audio metadata from tags embedded in the file using mutagen.
|
||||||
|
@ -71,34 +85,36 @@ def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
|
||||||
except (AttributeError, KeyError, IndexError):
|
except (AttributeError, KeyError, IndexError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
extracted_tags_mapping = {
|
extracted_tags_mapping = [
|
||||||
"title": "track_title",
|
("title", "track_title"),
|
||||||
"artist": "artist_name",
|
("artist", "artist_name"),
|
||||||
"album": "album_title",
|
("album", "album_title"),
|
||||||
"bpm": "bpm",
|
("bpm", "bpm"),
|
||||||
"composer": "composer",
|
("composer", "composer"),
|
||||||
"conductor": "conductor",
|
("conductor", "conductor"),
|
||||||
"copyright": "copyright",
|
("copyright", "copyright"),
|
||||||
"comment": "comment",
|
("comment", "comment"),
|
||||||
"encoded_by": "encoder",
|
("comment", "comments"),
|
||||||
"genre": "genre",
|
("comment", "description"),
|
||||||
"isrc": "isrc",
|
("encoded_by", "encoder"),
|
||||||
"label": "label",
|
("genre", "genre"),
|
||||||
"organization": "label",
|
("isrc", "isrc"),
|
||||||
# "length": "length",
|
("label", "label"),
|
||||||
"language": "language",
|
("organization", "label"),
|
||||||
"last_modified": "last_modified",
|
# ("length", "length"),
|
||||||
"mood": "mood",
|
("language", "language"),
|
||||||
"bit_rate": "bit_rate",
|
("last_modified", "last_modified"),
|
||||||
"replay_gain": "replaygain",
|
("mood", "mood"),
|
||||||
# "tracknumber": "track_number",
|
("bit_rate", "bit_rate"),
|
||||||
# "track_total": "track_total",
|
("replay_gain", "replaygain"),
|
||||||
"website": "website",
|
# ("tracknumber", "track_number"),
|
||||||
"date": "year",
|
# ("track_total", "track_total"),
|
||||||
# "mime_type": "mime",
|
("website", "website"),
|
||||||
}
|
("date", "year"),
|
||||||
|
# ("mime_type", "mime"),
|
||||||
|
]
|
||||||
|
|
||||||
for extracted_key, metadata_key in extracted_tags_mapping.items():
|
for extracted_key, metadata_key in extracted_tags_mapping:
|
||||||
try:
|
try:
|
||||||
metadata[metadata_key] = extracted[extracted_key]
|
metadata[metadata_key] = extracted[extracted_key]
|
||||||
if isinstance(metadata[metadata_key], list):
|
if isinstance(metadata[metadata_key], list):
|
||||||
|
|
|
@ -16,8 +16,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Step(Protocol):
|
class Step(Protocol):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __call__(filename: str, metadata: Dict[str, Any]):
|
def __call__(filename: str, metadata: Dict[str, Any]): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineStatus(int, Enum):
|
class PipelineStatus(int, Enum):
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
# This file is auto-generated by tools/extract_requirements.py.
|
# This file is auto-generated by tools/extract_requirements.py.
|
||||||
mutagen>=1.45.1,<1.48
|
mutagen>=1.45.1,<1.48
|
||||||
pika>=1.0.0,<1.4
|
pika>=1.0.0,<1.4
|
||||||
requests>=2.31.0,<2.32
|
requests>=2.32.2,<2.33
|
||||||
typing_extensions
|
typing_extensions
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
version = "4.0.0" # x-release-please-version
|
version = "4.2.0" # x-release-please-version
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="libretime-analyzer",
|
name="libretime-analyzer",
|
||||||
|
@ -24,7 +24,7 @@ setup(
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"mutagen>=1.45.1,<1.48",
|
"mutagen>=1.45.1,<1.48",
|
||||||
"pika>=1.0.0,<1.4",
|
"pika>=1.0.0,<1.4",
|
||||||
"requests>=2.31.0,<2.32",
|
"requests>=2.32.2,<2.33",
|
||||||
"typing_extensions",
|
"typing_extensions",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
|
|
|
@ -23,28 +23,28 @@ FILES = [
|
||||||
# 8s -> 9s: silence
|
# 8s -> 9s: silence
|
||||||
# 9s -> 12s: musik
|
# 9s -> 12s: musik
|
||||||
# 12s -> 15s: pink noise fade out
|
# 12s -> 15s: pink noise fade out
|
||||||
Fixture(here / "s1-jointstereo.mp3", 15.0, 6.0, 13.0, -5.9 ),
|
Fixture(here / "s1-jointstereo.mp3", 15.0, 1.4, 15.0, -5.9 ),
|
||||||
Fixture(here / "s1-mono.mp3", 15.0, 6.0, 13.0, -2.0 ),
|
Fixture(here / "s1-mono.mp3", 15.0, 1.5, 15.0, -2.0 ),
|
||||||
Fixture(here / "s1-stereo.mp3", 15.0, 6.0, 13.0, -5.9 ),
|
Fixture(here / "s1-stereo.mp3", 15.0, 1.4, 15.0, -5.9 ),
|
||||||
Fixture(here / "s1-mono-12.mp3", 15.0, 9.0, 12.0, +7.0 ),
|
Fixture(here / "s1-mono-12.mp3", 15.0, 1.2, 15.0, +7.0 ),
|
||||||
Fixture(here / "s1-stereo-12.mp3", 15.0, 9.0, 12.0, +6.1 ),
|
Fixture(here / "s1-stereo-12.mp3", 15.0, 1.2, 15.0, +6.1 ),
|
||||||
Fixture(here / "s1-mono+12.mp3", 15.0, 3.5, 13.0, -17.0 ),
|
Fixture(here / "s1-mono+12.mp3", 15.0, 1.2, 15.0, -17.0 ),
|
||||||
Fixture(here / "s1-stereo+12.mp3", 15.0, 3.5, 13.0, -17.8 ),
|
Fixture(here / "s1-stereo+12.mp3", 15.0, 1.2, 15.0, -17.8 ),
|
||||||
Fixture(here / "s1-mono.flac", 15.0, 6.0, 13.0, -2.3 ),
|
Fixture(here / "s1-mono.flac", 15.0, 1.4, 15.0, -2.3 ),
|
||||||
Fixture(here / "s1-stereo.flac", 15.0, 6.0, 13.0, -6.0 ),
|
Fixture(here / "s1-stereo.flac", 15.0, 1.4, 15.0, -6.0 ),
|
||||||
Fixture(here / "s1-mono-12.flac", 15.0, 9.0, 12.0, +10.0 ),
|
Fixture(here / "s1-mono-12.flac", 15.0, 2.0, 15.0, +10.0 ),
|
||||||
Fixture(here / "s1-stereo-12.flac", 15.0, 9.0, 12.0, +5.9 ),
|
Fixture(here / "s1-stereo-12.flac", 15.0, 1.8, 15.0, +5.9 ),
|
||||||
Fixture(here / "s1-mono+12.flac", 15.0, 3.5, 13.0, -12.0 ),
|
Fixture(here / "s1-mono+12.flac", 15.0, 0.0, 15.0, -12.0 ),
|
||||||
Fixture(here / "s1-stereo+12.flac", 15.0, 3.5, 13.0, -14.9 ),
|
Fixture(here / "s1-stereo+12.flac", 15.0, 0.0, 15.0, -14.9 ),
|
||||||
Fixture(here / "s1-mono.m4a", 15.0, 6.0, 13.0, -4.5 ),
|
Fixture(here / "s1-mono.m4a", 15.0, 1.4, 15.0, -4.5 ),
|
||||||
Fixture(here / "s1-stereo.m4a", 15.0, 6.0, 13.0, -5.8 ),
|
Fixture(here / "s1-stereo.m4a", 15.0, 1.4, 15.0, -5.8 ),
|
||||||
Fixture(here / "s1-mono.ogg", 15.0, 6.0, 13.0, -4.9 ),
|
Fixture(here / "s1-mono.ogg", 15.0, 1.4, 15.0, -4.9 ),
|
||||||
Fixture(here / "s1-stereo.ogg", 15.0, 6.0, 13.0, -5.7 ),
|
Fixture(here / "s1-stereo.ogg", 15.0, 1.4, 15.0, -5.7 ),
|
||||||
Fixture(here / "s1-stereo", 15.0, 6.0, 13.0, -5.7 ),
|
Fixture(here / "s1-stereo", 15.0, 1.4, 15.0, -5.7 ),
|
||||||
Fixture(here / "s1-mono.wav", 15.0, 6.0, 13.0, -2.3 ),
|
Fixture(here / "s1-mono.wav", 15.0, 1.5, 15.0, -2.3 ),
|
||||||
Fixture(here / "s1-stereo.wav", 15.0, 6.0, 13.0, -6.0 ),
|
Fixture(here / "s1-stereo.wav", 15.0, 1.4, 15.0, -6.0 ),
|
||||||
# sample 1 large (looped for 2 hours)
|
# sample 1 large (looped for 2 hours)
|
||||||
Fixture(here / "s1-large.flac", 7200, 6.0, 7198, -6.0 ),
|
Fixture(here / "s1-large.flac", 7200, 1.4, 7200, -6.0 ),
|
||||||
# sample 2
|
# sample 2
|
||||||
# 0s -> 1.8s: silence
|
# 0s -> 1.8s: silence
|
||||||
# 1.8s : noise
|
# 1.8s : noise
|
||||||
|
@ -96,12 +96,18 @@ tags = {
|
||||||
"comment": "Test Comment",
|
"comment": "Test Comment",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mp3Tags = {
|
||||||
|
**tags,
|
||||||
|
"comments": tags["comment"],
|
||||||
|
"description": tags["comment"],
|
||||||
|
}
|
||||||
|
|
||||||
FILES_TAGGED = [
|
FILES_TAGGED = [
|
||||||
FixtureMeta(
|
FixtureMeta(
|
||||||
here / "s1-jointstereo-tagged.mp3",
|
here / "s1-jointstereo-tagged.mp3",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(128000, abs=1e2),
|
"bit_rate": approx(128000, abs=1e2),
|
||||||
"channels": 2,
|
"channels": 2,
|
||||||
"mime": "audio/mp3",
|
"mime": "audio/mp3",
|
||||||
|
@ -111,7 +117,7 @@ FILES_TAGGED = [
|
||||||
here / "s1-mono-tagged.mp3",
|
here / "s1-mono-tagged.mp3",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(64000, abs=1e2),
|
"bit_rate": approx(64000, abs=1e2),
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"mime": "audio/mp3",
|
"mime": "audio/mp3",
|
||||||
|
@ -121,7 +127,7 @@ FILES_TAGGED = [
|
||||||
here / "s1-stereo-tagged.mp3",
|
here / "s1-stereo-tagged.mp3",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(128000, abs=1e2),
|
"bit_rate": approx(128000, abs=1e2),
|
||||||
"channels": 2,
|
"channels": 2,
|
||||||
"mime": "audio/mp3",
|
"mime": "audio/mp3",
|
||||||
|
@ -151,7 +157,7 @@ FILES_TAGGED = [
|
||||||
here / "s1-mono-tagged.m4a",
|
here / "s1-mono-tagged.m4a",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(65000, abs=5e4),
|
"bit_rate": approx(65000, abs=5e4),
|
||||||
"channels": 2, # Weird
|
"channels": 2, # Weird
|
||||||
"mime": "audio/mp4",
|
"mime": "audio/mp4",
|
||||||
|
@ -161,7 +167,7 @@ FILES_TAGGED = [
|
||||||
here / "s1-stereo-tagged.m4a",
|
here / "s1-stereo-tagged.m4a",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(128000, abs=1e5),
|
"bit_rate": approx(128000, abs=1e5),
|
||||||
"channels": 2,
|
"channels": 2,
|
||||||
"mime": "audio/mp4",
|
"mime": "audio/mp4",
|
||||||
|
@ -228,12 +234,18 @@ tags = {
|
||||||
"comment": "Ł Ą Ż Ę Ć Ń Ś Ź",
|
"comment": "Ł Ą Ż Ę Ć Ń Ś Ź",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mp3Tags = {
|
||||||
|
**tags,
|
||||||
|
"comments": tags["comment"],
|
||||||
|
"description": tags["comment"],
|
||||||
|
}
|
||||||
|
|
||||||
FILES_TAGGED += [
|
FILES_TAGGED += [
|
||||||
FixtureMeta(
|
FixtureMeta(
|
||||||
here / "s1-jointstereo-tagged-utf8.mp3",
|
here / "s1-jointstereo-tagged-utf8.mp3",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(128000, abs=1e2),
|
"bit_rate": approx(128000, abs=1e2),
|
||||||
"channels": 2,
|
"channels": 2,
|
||||||
"mime": "audio/mp3",
|
"mime": "audio/mp3",
|
||||||
|
@ -243,7 +255,7 @@ FILES_TAGGED += [
|
||||||
here / "s1-mono-tagged-utf8.mp3",
|
here / "s1-mono-tagged-utf8.mp3",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(64000, abs=1e2),
|
"bit_rate": approx(64000, abs=1e2),
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"mime": "audio/mp3",
|
"mime": "audio/mp3",
|
||||||
|
@ -253,7 +265,7 @@ FILES_TAGGED += [
|
||||||
here / "s1-stereo-tagged-utf8.mp3",
|
here / "s1-stereo-tagged-utf8.mp3",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(128000, abs=1e2),
|
"bit_rate": approx(128000, abs=1e2),
|
||||||
"channels": 2,
|
"channels": 2,
|
||||||
"mime": "audio/mp3",
|
"mime": "audio/mp3",
|
||||||
|
@ -283,7 +295,7 @@ FILES_TAGGED += [
|
||||||
here / "s1-mono-tagged-utf8.m4a",
|
here / "s1-mono-tagged-utf8.m4a",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(65000, abs=5e4),
|
"bit_rate": approx(65000, abs=5e4),
|
||||||
"channels": 2, # Weird
|
"channels": 2, # Weird
|
||||||
"mime": "audio/mp4",
|
"mime": "audio/mp4",
|
||||||
|
@ -293,7 +305,7 @@ FILES_TAGGED += [
|
||||||
here / "s1-stereo-tagged-utf8.m4a",
|
here / "s1-stereo-tagged-utf8.m4a",
|
||||||
{
|
{
|
||||||
**meta,
|
**meta,
|
||||||
**tags,
|
**mp3Tags,
|
||||||
"bit_rate": approx(128000, abs=1e5),
|
"bit_rate": approx(128000, abs=1e5),
|
||||||
"channels": 2,
|
"channels": 2,
|
||||||
"mime": "audio/mp4",
|
"mime": "audio/mp4",
|
||||||
|
|
|
@ -27,8 +27,8 @@ def test_analyze_metadata(filepath: Path, metadata: dict):
|
||||||
del metadata["length"]
|
del metadata["length"]
|
||||||
del found["length"]
|
del found["length"]
|
||||||
|
|
||||||
# mp3,ogg,flac files does not support comments yet
|
# ogg,flac files does not support comments yet
|
||||||
if not filepath.suffix == ".m4a":
|
if not filepath.suffix == ".m4a" and not filepath.suffix == ".mp3":
|
||||||
if "comment" in metadata:
|
if "comment" in metadata:
|
||||||
del metadata["comment"]
|
del metadata["comment"]
|
||||||
|
|
||||||
|
|
|
@ -214,3 +214,6 @@ class ApiClient:
|
||||||
|
|
||||||
def update_metadata_on_tunein(self):
|
def update_metadata_on_tunein(self):
|
||||||
self._base_client.update_metadata_on_tunein()
|
self._base_client.update_metadata_on_tunein()
|
||||||
|
|
||||||
|
def trigger_task_manager(self):
|
||||||
|
self._base_client.version()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Please do not edit this file, edit the setup.py file!
|
# Please do not edit this file, edit the setup.py file!
|
||||||
# This file is auto-generated by tools/extract_requirements.py.
|
# This file is auto-generated by tools/extract_requirements.py.
|
||||||
python-dateutil>=2.8.1,<2.9
|
python-dateutil>=2.8.1,<2.10
|
||||||
requests>=2.31.0,<2.32
|
requests>=2.32.2,<2.33
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
version = "4.0.0" # x-release-please-version
|
version = "4.2.0" # x-release-please-version
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="libretime-api-client",
|
name="libretime-api-client",
|
||||||
|
@ -18,8 +18,8 @@ setup(
|
||||||
package_data={"": ["py.typed"]},
|
package_data={"": ["py.typed"]},
|
||||||
python_requires=">=3.8",
|
python_requires=">=3.8",
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"python-dateutil>=2.8.1,<2.9",
|
"python-dateutil>=2.8.1,<2.10",
|
||||||
"requests>=2.31.0,<2.32",
|
"requests>=2.32.2,<2.33",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
"dev": [
|
"dev": [
|
||||||
|
|
|
@ -18,12 +18,13 @@ class StreamPreferences(BaseModel):
|
||||||
input_fade_transition: float
|
input_fade_transition: float
|
||||||
message_format: MessageFormatKind
|
message_format: MessageFormatKind
|
||||||
message_offline: str
|
message_offline: str
|
||||||
|
replay_gain_enabled: bool
|
||||||
|
replay_gain_offset: float
|
||||||
|
|
||||||
# input_auto_switch_off: bool
|
# input_auto_switch_off: bool
|
||||||
# input_auto_switch_on: bool
|
# input_auto_switch_on: bool
|
||||||
# input_main_user: str
|
# input_main_user: str
|
||||||
# input_main_password: str
|
# input_main_password: str
|
||||||
# replay_gain_enabled: bool
|
|
||||||
# replay_gain_offset: float
|
|
||||||
# track_fade_in: float
|
# track_fade_in: float
|
||||||
# track_fade_out: float
|
# track_fade_out: float
|
||||||
# track_fade_transition: float
|
# track_fade_transition: float
|
||||||
|
@ -82,6 +83,8 @@ class Preference(models.Model):
|
||||||
int(entries.get("stream_label_format") or 0)
|
int(entries.get("stream_label_format") or 0)
|
||||||
),
|
),
|
||||||
message_offline=entries.get("off_air_meta") or "Offline",
|
message_offline=entries.get("off_air_meta") or "Offline",
|
||||||
|
replay_gain_enabled=entries.get("enable_replay_gain") == "1",
|
||||||
|
replay_gain_offset=float(entries.get("replay_gain_modifier") or 0.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -8,6 +8,7 @@ from .role import Role
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
|
# pylint: disable=too-many-positional-arguments
|
||||||
def create_user(self, role, username, password, email, first_name, last_name):
|
def create_user(self, role, username, password, email, first_name, last_name):
|
||||||
user = self.model(
|
user = self.model(
|
||||||
role=role,
|
role=role,
|
||||||
|
@ -20,6 +21,7 @@ class UserManager(BaseUserManager):
|
||||||
user.save(using=self._db)
|
user.save(using=self._db)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
# pylint: disable=too-many-positional-arguments
|
||||||
def create_superuser(self, username, password, email, first_name, last_name):
|
def create_superuser(self, username, password, email, first_name, last_name):
|
||||||
return self.create_user(
|
return self.create_user(
|
||||||
Role.ADMIN,
|
Role.ADMIN,
|
||||||
|
|
|
@ -6,6 +6,8 @@ class StreamPreferencesSerializer(serializers.Serializer):
|
||||||
input_fade_transition = serializers.FloatField(read_only=True)
|
input_fade_transition = serializers.FloatField(read_only=True)
|
||||||
message_format = serializers.IntegerField(read_only=True)
|
message_format = serializers.IntegerField(read_only=True)
|
||||||
message_offline = serializers.CharField(read_only=True)
|
message_offline = serializers.CharField(read_only=True)
|
||||||
|
replay_gain_enabled = serializers.BooleanField(read_only=True)
|
||||||
|
replay_gain_offset = serializers.FloatField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
|
|
|
@ -16,6 +16,8 @@ def test_preference_get_stream_preferences(db):
|
||||||
"input_fade_transition": 0.0,
|
"input_fade_transition": 0.0,
|
||||||
"message_format": 0,
|
"message_format": 0,
|
||||||
"message_offline": "LibreTime - offline",
|
"message_offline": "LibreTime - offline",
|
||||||
|
"replay_gain_enabled": True,
|
||||||
|
"replay_gain_offset": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ def test_stream_preferences_get(db, api_client: APIClient):
|
||||||
"input_fade_transition": 0.0,
|
"input_fade_transition": 0.0,
|
||||||
"message_format": 0,
|
"message_format": 0,
|
||||||
"message_offline": "LibreTime - offline",
|
"message_offline": "LibreTime - offline",
|
||||||
|
"replay_gain_enabled": True,
|
||||||
|
"replay_gain_offset": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ class StreamPreferencesView(views.APIView):
|
||||||
"input_fade_transition",
|
"input_fade_transition",
|
||||||
"message_format",
|
"message_format",
|
||||||
"message_offline",
|
"message_offline",
|
||||||
|
"replay_gain_enabled",
|
||||||
|
"replay_gain_offset",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,8 +8,6 @@ UP = """
|
||||||
-- DELETE FROM cc_pref WHERE keystr = 'system_version';
|
-- DELETE FROM cc_pref WHERE keystr = 'system_version';
|
||||||
-- INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '2.5.5');
|
-- INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '2.5.5');
|
||||||
|
|
||||||
ALTER TABLE cc_show ADD COLUMN image_path varchar(255) DEFAULT '';
|
|
||||||
ALTER TABLE cc_show_instances ADD COLUMN description varchar(255) DEFAULT '';
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DOWN = None
|
DOWN = None
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.db import migrations
|
||||||
from ._migrations import legacy_migration_factory
|
from ._migrations import legacy_migration_factory
|
||||||
|
|
||||||
UP = """
|
UP = """
|
||||||
ALTER TABLE cc_files ADD COLUMN artwork TYPE character varying(255);
|
ALTER TABLE cc_files ADD COLUMN artwork VARCHAR(255);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DOWN = None
|
DOWN = None
|
||||||
|
|
|
@ -4,12 +4,18 @@ from django.db import migrations
|
||||||
|
|
||||||
from ._migrations import legacy_migration_factory
|
from ._migrations import legacy_migration_factory
|
||||||
|
|
||||||
|
# This migration is currently a placeholder for 3.0.0-alpha.9.1.
|
||||||
|
# Please do not remove it. There are currently no actions, but it
|
||||||
|
# needs to remain intact so it does not fail when called from the
|
||||||
|
# migrations script. Any future migrations that may apply to
|
||||||
|
# 3.0.0-alpha.9.1 will be added to this file.
|
||||||
|
|
||||||
UP = """
|
UP = """
|
||||||
ALTER TABLE cc_files ADD COLUMN artwork VARCHAR(4096);
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DOWN = """
|
DOWN = """
|
||||||
ALTER TABLE cc_files DROP COLUMN IF EXISTS artwork;
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
# 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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
|
@ -1,2 +1,2 @@
|
||||||
# The schema version is defined using the migration file prefix number
|
# The schema version is defined using the migration file prefix number
|
||||||
LEGACY_SCHEMA_VERSION = "45"
|
LEGACY_SCHEMA_VERSION = "46"
|
||||||
|
|
|
@ -11,15 +11,28 @@ def get_schema_version():
|
||||||
|
|
||||||
Don't use django models as they might break in the future. Our concern is to upgrade
|
Don't use django models as they might break in the future. Our concern is to upgrade
|
||||||
the legacy database schema to the point where django is in charge of the migrations.
|
the legacy database schema to the point where django is in charge of the migrations.
|
||||||
|
|
||||||
|
An airtime 2.5.1 migration will not have schema_version, in that case, we look for
|
||||||
|
system_version to have a value of 2.5.1 and return that as the schema version value
|
||||||
|
(really just needs to be anything besides None, so that the next migration doesn't overwrite
|
||||||
|
the database)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if "cc_pref" not in connection.introspection.table_names():
|
if "cc_pref" not in connection.introspection.table_names():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute("SELECT valstr FROM cc_pref WHERE keystr = 'schema_version'")
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT valstr AS version
|
||||||
|
FROM cc_pref
|
||||||
|
WHERE (keystr = 'schema_version') OR (keystr = 'system_version' AND valstr = '2.5.1')
|
||||||
|
"""
|
||||||
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
return row[0] if row else None
|
if row and row[0]:
|
||||||
|
return row[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def set_schema_version(cursor, version: str):
|
def set_schema_version(cursor, version: str):
|
||||||
|
|
|
@ -126,6 +126,10 @@ CREATE TABLE "cc_show"
|
||||||
"has_autoplaylist" BOOLEAN DEFAULT 'f' NOT NULL,
|
"has_autoplaylist" BOOLEAN DEFAULT 'f' NOT NULL,
|
||||||
"autoplaylist_id" INTEGER,
|
"autoplaylist_id" INTEGER,
|
||||||
"autoplaylist_repeat" BOOLEAN DEFAULT 'f' NOT NULL,
|
"autoplaylist_repeat" BOOLEAN DEFAULT 'f' NOT NULL,
|
||||||
|
"override_intro_playlist" BOOLEAN DEFAULT 'f' NOT NULL,
|
||||||
|
"intro_playlist_id" INTEGER,
|
||||||
|
"override_outro_playlist" BOOLEAN DEFAULT 'f' NOT NULL,
|
||||||
|
"outro_playlist_id" INTEGER,
|
||||||
PRIMARY KEY ("id")
|
PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -718,6 +722,16 @@ ALTER TABLE "cc_show" ADD CONSTRAINT "cc_playlist_autoplaylist_fkey"
|
||||||
REFERENCES "cc_playlist" ("id")
|
REFERENCES "cc_playlist" ("id")
|
||||||
ON DELETE SET NULL;
|
ON DELETE SET 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 CONSTRAINT "cc_playlist_outro_playlist_fkey"
|
||||||
|
FOREIGN KEY ("outro_playlist_id")
|
||||||
|
REFERENCES "cc_playlist" ("id")
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
ALTER TABLE "cc_show_instances" ADD CONSTRAINT "cc_show_fkey"
|
ALTER TABLE "cc_show_instances" ADD CONSTRAINT "cc_show_fkey"
|
||||||
FOREIGN KEY ("show_id")
|
FOREIGN KEY ("show_id")
|
||||||
REFERENCES "cc_show" ("id")
|
REFERENCES "cc_show" ("id")
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .readwriteserializer import ReadWriteSerializerMixin
|
|
@ -0,0 +1,33 @@
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
|
|
||||||
|
class ReadWriteSerializerMixin:
|
||||||
|
"""
|
||||||
|
Overrides get_serializer_class to choose the read serializer
|
||||||
|
for GET requests and the write serializer for POST requests.
|
||||||
|
|
||||||
|
Set read_serializer_class and write_serializer_class attributes on a
|
||||||
|
viewset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
read_serializer_class = Serializer
|
||||||
|
write_serializer_class = Serializer
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action in ["create"]:
|
||||||
|
return self.get_write_serializer_class()
|
||||||
|
return self.get_read_serializer_class()
|
||||||
|
|
||||||
|
def get_read_serializer_class(self):
|
||||||
|
assert self.read_serializer_class is not None, (
|
||||||
|
f"'{self.__class__.__name__}' should either include a `read_serializer_class`"
|
||||||
|
"attribute, or override the `get_read_serializer_class()` method."
|
||||||
|
)
|
||||||
|
return self.read_serializer_class
|
||||||
|
|
||||||
|
def get_write_serializer_class(self):
|
||||||
|
assert self.write_serializer_class is not None, (
|
||||||
|
f"'{self.__class__.__name__}' should either include a `write_serializer_class`"
|
||||||
|
"attribute, or override the `get_write_serializer_class()` method."
|
||||||
|
)
|
||||||
|
return self.write_serializer_class
|
|
@ -1,4 +1,5 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
|
||||||
class Schedule(models.Model):
|
class Schedule(models.Model):
|
||||||
|
@ -115,6 +116,14 @@ class Schedule(models.Model):
|
||||||
return self.instance.ends_at
|
return self.instance.ends_at
|
||||||
return self.ends_at
|
return self.ends_at
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_file_scheduled_in_the_future(file_id):
|
||||||
|
count = Schedule.objects.filter(
|
||||||
|
file_id=file_id,
|
||||||
|
ends_at__gt=now(),
|
||||||
|
).count()
|
||||||
|
return count > 0
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
managed = False
|
managed = False
|
||||||
db_table = "cc_schedule"
|
db_table = "cc_schedule"
|
||||||
|
|
|
@ -69,6 +69,28 @@ class Show(models.Model):
|
||||||
auto_playlist_enabled = models.BooleanField(db_column="has_autoplaylist")
|
auto_playlist_enabled = models.BooleanField(db_column="has_autoplaylist")
|
||||||
auto_playlist_repeat = models.BooleanField(db_column="autoplaylist_repeat")
|
auto_playlist_repeat = models.BooleanField(db_column="autoplaylist_repeat")
|
||||||
|
|
||||||
|
intro_playlist = models.ForeignKey(
|
||||||
|
"schedule.Playlist",
|
||||||
|
on_delete=models.DO_NOTHING,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
db_column="intro_playlist_id",
|
||||||
|
related_name="intro_playlist",
|
||||||
|
)
|
||||||
|
|
||||||
|
override_intro_playlist = models.BooleanField(db_column="override_intro_playlist")
|
||||||
|
|
||||||
|
outro_playlist = models.ForeignKey(
|
||||||
|
"schedule.Playlist",
|
||||||
|
on_delete=models.DO_NOTHING,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
db_column="outro_playlist_id",
|
||||||
|
related_name="outro_playlist",
|
||||||
|
)
|
||||||
|
|
||||||
|
override_outro_playlist = models.BooleanField(db_column="override_outro_playlist")
|
||||||
|
|
||||||
hosts = models.ManyToManyField( # type: ignore[var-annotated]
|
hosts = models.ManyToManyField( # type: ignore[var-annotated]
|
||||||
"core.User",
|
"core.User",
|
||||||
through="ShowHost",
|
through="ShowHost",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from .playlist import PlaylistContentSerializer, PlaylistSerializer
|
from .playlist import PlaylistContentSerializer, PlaylistSerializer
|
||||||
from .schedule import ScheduleSerializer
|
from .schedule import ReadScheduleSerializer, WriteScheduleSerializer
|
||||||
from .show import (
|
from .show import (
|
||||||
ShowDaysSerializer,
|
ShowDaysSerializer,
|
||||||
ShowHostSerializer,
|
ShowHostSerializer,
|
||||||
|
|
|
@ -3,10 +3,17 @@ from rest_framework import serializers
|
||||||
from ..models import Schedule
|
from ..models import Schedule
|
||||||
|
|
||||||
|
|
||||||
class ScheduleSerializer(serializers.ModelSerializer):
|
class ReadScheduleSerializer(serializers.ModelSerializer):
|
||||||
cue_out = serializers.DurationField(source="get_cue_out", read_only=True)
|
cue_out = serializers.DurationField(source="get_cue_out", read_only=True)
|
||||||
ends_at = serializers.DateTimeField(source="get_ends_at", read_only=True)
|
ends_at = serializers.DateTimeField(source="get_ends_at", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Schedule
|
model = Schedule
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class WriteScheduleSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Schedule
|
||||||
|
fields = "__all__"
|
||||||
|
|
|
@ -21,6 +21,10 @@ class ShowSerializer(serializers.ModelSerializer):
|
||||||
"auto_playlist",
|
"auto_playlist",
|
||||||
"auto_playlist_enabled",
|
"auto_playlist_enabled",
|
||||||
"auto_playlist_repeat",
|
"auto_playlist_repeat",
|
||||||
|
"intro_playlist",
|
||||||
|
"override_intro_playlist",
|
||||||
|
"outro_playlist",
|
||||||
|
"override_outro_playlist",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,9 @@ from django.db import models
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from ...mixins import ReadWriteSerializerMixin
|
||||||
from ..models import Schedule
|
from ..models import Schedule
|
||||||
from ..serializers import ScheduleSerializer
|
from ..serializers import ReadScheduleSerializer, WriteScheduleSerializer
|
||||||
|
|
||||||
|
|
||||||
class ScheduleFilter(filters.FilterSet):
|
class ScheduleFilter(filters.FilterSet):
|
||||||
|
@ -26,8 +27,9 @@ class ScheduleFilter(filters.FilterSet):
|
||||||
fields = [] # type: ignore
|
fields = [] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class ScheduleViewSet(viewsets.ModelViewSet):
|
class ScheduleViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
|
||||||
queryset = Schedule.objects.all()
|
queryset = Schedule.objects.all()
|
||||||
serializer_class = ScheduleSerializer
|
read_serializer_class = ReadScheduleSerializer
|
||||||
|
write_serializer_class = WriteScheduleSerializer
|
||||||
filterset_class = ScheduleFilter
|
filterset_class = ScheduleFilter
|
||||||
model_permission_name = "schedule"
|
model_permission_name = "schedule"
|
||||||
|
|
|
@ -1,35 +1,96 @@
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from ...._fixtures import AUDIO_FILENAME
|
from ...._fixtures import AUDIO_FILENAME
|
||||||
|
from ...models import File
|
||||||
|
|
||||||
|
|
||||||
class TestFileViewSet(APITestCase):
|
class TestFileViewSet(APITestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.path = "/api/v2/files/{id}/download"
|
|
||||||
cls.token = settings.CONFIG.general.api_key
|
cls.token = settings.CONFIG.general.api_key
|
||||||
|
|
||||||
def test_invalid(self):
|
def test_download_invalid(self):
|
||||||
path = self.path.format(id="a")
|
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
||||||
response = self.client.get(path)
|
file_id = "1"
|
||||||
self.assertEqual(response.status_code, 400)
|
response = self.client.get(f"/api/v2/files/{file_id}/download")
|
||||||
|
|
||||||
def test_does_not_exist(self):
|
|
||||||
path = self.path.format(id="1")
|
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
|
||||||
response = self.client.get(path)
|
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def test_exists(self):
|
def test_download(self):
|
||||||
file = baker.make(
|
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
||||||
|
file: File = baker.make(
|
||||||
"storage.File",
|
"storage.File",
|
||||||
mime="audio/mp3",
|
mime="audio/mp3",
|
||||||
filepath=AUDIO_FILENAME,
|
filepath=AUDIO_FILENAME,
|
||||||
)
|
)
|
||||||
path = self.path.format(id=str(file.pk))
|
response = self.client.get(f"/api/v2/files/{file.id}/download")
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
|
||||||
response = self.client.get(path)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_destroy(self):
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
||||||
|
file: File = baker.make(
|
||||||
|
"storage.File",
|
||||||
|
mime="audio/mp3",
|
||||||
|
filepath=AUDIO_FILENAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("libretime_api.storage.views.file.remove") as remove_mock:
|
||||||
|
response = self.client.delete(f"/api/v2/files/{file.id}")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
remove_mock.assert_called_with(
|
||||||
|
os.path.join(settings.CONFIG.storage.path, file.filepath)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_destroy_no_file(self):
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
||||||
|
file = baker.make(
|
||||||
|
"storage.File",
|
||||||
|
mime="audio/mp3",
|
||||||
|
filepath="invalid.mp3",
|
||||||
|
)
|
||||||
|
response = self.client.delete(f"/api/v2/files/{file.id}")
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
|
def test_destroy_invalid(self):
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
||||||
|
file_id = "1"
|
||||||
|
response = self.client.delete(f"/api/v2/files/{file_id}")
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_filters(self):
|
||||||
|
file = baker.make(
|
||||||
|
"storage.File",
|
||||||
|
mime="audio/mp3",
|
||||||
|
filepath=AUDIO_FILENAME,
|
||||||
|
genre="Soul",
|
||||||
|
md5="5a11ffe0e6c6d70fcdbad1b734be6482",
|
||||||
|
)
|
||||||
|
baker.make(
|
||||||
|
"storage.File",
|
||||||
|
mime="audio/mp3",
|
||||||
|
filepath=AUDIO_FILENAME,
|
||||||
|
genre="R&B",
|
||||||
|
md5="5a11ffe0e6c6d70fcdbad1b734be6483",
|
||||||
|
)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f"Api-Key {self.token}")
|
||||||
|
|
||||||
|
path = "/api/v2/files"
|
||||||
|
results = self.client.get(path).json()
|
||||||
|
self.assertEqual(len(results), 2)
|
||||||
|
|
||||||
|
path = f"/api/v2/files?md5={file.md5}"
|
||||||
|
results = self.client.get(path).json()
|
||||||
|
self.assertEqual(len(results), 1)
|
||||||
|
|
||||||
|
path = "/api/v2/files?genre=Soul"
|
||||||
|
results = self.client.get(path).json()
|
||||||
|
self.assertEqual(len(results), 1)
|
||||||
|
|
||||||
|
path = "/api/v2/files?genre=R%26B"
|
||||||
|
results = self.client.get(path).json()
|
||||||
|
self.assertEqual(len(results), 1)
|
||||||
|
|
|
@ -1,31 +1,62 @@
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from os import remove
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.utils.encoding import filepath_to_uri
|
from django.utils.encoding import filepath_to_uri
|
||||||
from rest_framework import viewsets
|
from django_filters import rest_framework as filters
|
||||||
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.serializers import IntegerField
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
from ...schedule.models import Schedule
|
||||||
from ..models import File
|
from ..models import File
|
||||||
from ..serializers import FileSerializer
|
from ..serializers import FileSerializer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FileInUse(APIException):
|
||||||
|
status_code = status.HTTP_409_CONFLICT
|
||||||
|
default_detail = "The file is currently used"
|
||||||
|
default_code = "file_in_use"
|
||||||
|
|
||||||
|
|
||||||
class FileViewSet(viewsets.ModelViewSet):
|
class FileViewSet(viewsets.ModelViewSet):
|
||||||
queryset = File.objects.all()
|
queryset = File.objects.all()
|
||||||
serializer_class = FileSerializer
|
serializer_class = FileSerializer
|
||||||
model_permission_name = "file"
|
model_permission_name = "file"
|
||||||
|
filter_backends = (filters.DjangoFilterBackend,)
|
||||||
|
filterset_fields = ("md5", "genre")
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name,unused-argument
|
||||||
@action(detail=True, methods=["GET"])
|
@action(detail=True, methods=["GET"])
|
||||||
def download(self, request, pk=None): # pylint: disable=invalid-name
|
def download(self, request, pk=None):
|
||||||
pk = IntegerField().to_internal_value(data=pk)
|
instance: File = self.get_object()
|
||||||
|
|
||||||
file = get_object_or_404(File, pk=pk)
|
|
||||||
|
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
|
|
||||||
# HTTP headers must be USASCII encoded, or Nginx might not find the file and
|
# HTTP headers must be USASCII encoded, or Nginx might not find the file and
|
||||||
# will return a 404.
|
# will return a 404.
|
||||||
redirect_uri = filepath_to_uri(os.path.join("/api/_media", file.filepath))
|
redirect_uri = filepath_to_uri(os.path.join("/api/_media", instance.filepath))
|
||||||
response["X-Accel-Redirect"] = redirect_uri
|
response["X-Accel-Redirect"] = redirect_uri
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def perform_destroy(self, instance: File):
|
||||||
|
if Schedule.is_file_scheduled_in_the_future(file_id=instance.id):
|
||||||
|
raise FileInUse("file is scheduled in the future")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if instance.filepath is None:
|
||||||
|
logger.warning("file does not have a filepath: %d", instance.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
path = os.path.join(settings.CONFIG.storage.path, instance.filepath)
|
||||||
|
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
logger.warning("file does not exist in storage: %d", instance.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
remove(path)
|
||||||
|
except OSError as exception:
|
||||||
|
raise APIException("could not delete file from storage") from exception
|
||||||
|
|
|
@ -4,6 +4,7 @@ URL Configuration
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# Please do not edit this file, edit the setup.py file!
|
# Please do not edit this file, edit the setup.py file!
|
||||||
# This file is auto-generated by tools/extract_requirements.py.
|
# This file is auto-generated by tools/extract_requirements.py.
|
||||||
django-cors-headers>=3.14.0,<4.4
|
django-cors-headers>=3.14.0,<4.5
|
||||||
django-filter>=2.4.0,<23.6
|
django-filter>=2.4.0,<24.4
|
||||||
django>=4.2.0,<4.3
|
django>=4.2.0,<4.3
|
||||||
djangorestframework>=3.14.0,<3.15
|
djangorestframework>=3.14.0,<3.16
|
||||||
drf-spectacular>=0.22.1,<0.28
|
drf-spectacular>=0.22.1,<0.29
|
||||||
gunicorn>=20.1.0,<21.3
|
gunicorn>=22.0.0,<23.1
|
||||||
psycopg[c]>=3.1.8,<3.2
|
psycopg[c]>=3.1.8,<3.2
|
||||||
requests>=2.31.0,<2.32
|
requests>=2.32.2,<2.33
|
||||||
uvicorn[standard]>=0.17.6,<0.26.0
|
uvicorn[standard]>=0.17.6,<0.33.0
|
||||||
|
|
236
api/schema.yml
236
api/schema.yml
|
@ -154,6 +154,15 @@ paths:
|
||||||
/api/v2/files:
|
/api/v2/files:
|
||||||
get:
|
get:
|
||||||
operationId: files_list
|
operationId: files_list
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: genre
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: md5
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
tags:
|
tags:
|
||||||
- files
|
- files
|
||||||
security:
|
security:
|
||||||
|
@ -2552,6 +2561,12 @@ paths:
|
||||||
/api/v2/schedule:
|
/api/v2/schedule:
|
||||||
get:
|
get:
|
||||||
operationId: schedule_list
|
operationId: schedule_list
|
||||||
|
description: |-
|
||||||
|
Overrides get_serializer_class to choose the read serializer
|
||||||
|
for GET requests and the write serializer for POST requests.
|
||||||
|
|
||||||
|
Set read_serializer_class and write_serializer_class attributes on a
|
||||||
|
viewset.
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: broadcasted
|
name: broadcasted
|
||||||
|
@ -2597,23 +2612,29 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/ReadSchedule"
|
||||||
description: ""
|
description: ""
|
||||||
post:
|
post:
|
||||||
operationId: schedule_create
|
operationId: schedule_create
|
||||||
|
description: |-
|
||||||
|
Overrides get_serializer_class to choose the read serializer
|
||||||
|
for GET requests and the write serializer for POST requests.
|
||||||
|
|
||||||
|
Set read_serializer_class and write_serializer_class attributes on a
|
||||||
|
viewset.
|
||||||
tags:
|
tags:
|
||||||
- schedule
|
- schedule
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/WriteSchedule"
|
||||||
application/x-www-form-urlencoded:
|
application/x-www-form-urlencoded:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/WriteSchedule"
|
||||||
multipart/form-data:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/WriteSchedule"
|
||||||
required: true
|
required: true
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
|
@ -2623,11 +2644,17 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/WriteSchedule"
|
||||||
description: ""
|
description: ""
|
||||||
/api/v2/schedule/{id}:
|
/api/v2/schedule/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: schedule_retrieve
|
operationId: schedule_retrieve
|
||||||
|
description: |-
|
||||||
|
Overrides get_serializer_class to choose the read serializer
|
||||||
|
for GET requests and the write serializer for POST requests.
|
||||||
|
|
||||||
|
Set read_serializer_class and write_serializer_class attributes on a
|
||||||
|
viewset.
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
|
@ -2645,10 +2672,16 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/ReadSchedule"
|
||||||
description: ""
|
description: ""
|
||||||
put:
|
put:
|
||||||
operationId: schedule_update
|
operationId: schedule_update
|
||||||
|
description: |-
|
||||||
|
Overrides get_serializer_class to choose the read serializer
|
||||||
|
for GET requests and the write serializer for POST requests.
|
||||||
|
|
||||||
|
Set read_serializer_class and write_serializer_class attributes on a
|
||||||
|
viewset.
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
|
@ -2662,13 +2695,13 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/ReadSchedule"
|
||||||
application/x-www-form-urlencoded:
|
application/x-www-form-urlencoded:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/ReadSchedule"
|
||||||
multipart/form-data:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/ReadSchedule"
|
||||||
required: true
|
required: true
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
|
@ -2678,10 +2711,16 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/ReadSchedule"
|
||||||
description: ""
|
description: ""
|
||||||
patch:
|
patch:
|
||||||
operationId: schedule_partial_update
|
operationId: schedule_partial_update
|
||||||
|
description: |-
|
||||||
|
Overrides get_serializer_class to choose the read serializer
|
||||||
|
for GET requests and the write serializer for POST requests.
|
||||||
|
|
||||||
|
Set read_serializer_class and write_serializer_class attributes on a
|
||||||
|
viewset.
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
|
@ -2695,13 +2734,13 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PatchedSchedule"
|
$ref: "#/components/schemas/PatchedReadSchedule"
|
||||||
application/x-www-form-urlencoded:
|
application/x-www-form-urlencoded:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PatchedSchedule"
|
$ref: "#/components/schemas/PatchedReadSchedule"
|
||||||
multipart/form-data:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PatchedSchedule"
|
$ref: "#/components/schemas/PatchedReadSchedule"
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
- basicAuth: []
|
- basicAuth: []
|
||||||
|
@ -2710,10 +2749,16 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Schedule"
|
$ref: "#/components/schemas/ReadSchedule"
|
||||||
description: ""
|
description: ""
|
||||||
delete:
|
delete:
|
||||||
operationId: schedule_destroy
|
operationId: schedule_destroy
|
||||||
|
description: |-
|
||||||
|
Overrides get_serializer_class to choose the read serializer
|
||||||
|
for GET requests and the write serializer for POST requests.
|
||||||
|
|
||||||
|
Set read_serializer_class and write_serializer_class attributes on a
|
||||||
|
viewset.
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: id
|
name: id
|
||||||
|
@ -6300,7 +6345,7 @@ components:
|
||||||
user:
|
user:
|
||||||
type: integer
|
type: integer
|
||||||
nullable: true
|
nullable: true
|
||||||
PatchedSchedule:
|
PatchedReadSchedule:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
@ -6409,6 +6454,16 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
auto_playlist_repeat:
|
auto_playlist_repeat:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
intro_playlist:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
override_intro_playlist:
|
||||||
|
type: boolean
|
||||||
|
outro_playlist:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
override_outro_playlist:
|
||||||
|
type: boolean
|
||||||
PatchedShowDays:
|
PatchedShowDays:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -7086,41 +7141,7 @@ components:
|
||||||
- id
|
- id
|
||||||
- key
|
- key
|
||||||
- user
|
- user
|
||||||
RecordEnabledEnum:
|
ReadSchedule:
|
||||||
enum:
|
|
||||||
- 0
|
|
||||||
- 1
|
|
||||||
type: integer
|
|
||||||
description: |-
|
|
||||||
* `0` - No
|
|
||||||
* `1` - Yes
|
|
||||||
RepeatKindEnum:
|
|
||||||
enum:
|
|
||||||
- 0
|
|
||||||
- 1
|
|
||||||
- 4
|
|
||||||
- 5
|
|
||||||
- 2
|
|
||||||
type: integer
|
|
||||||
description: |-
|
|
||||||
* `0` - Every week
|
|
||||||
* `1` - Every 2 weeks
|
|
||||||
* `4` - Every 3 weeks
|
|
||||||
* `5` - Every 4 weeks
|
|
||||||
* `2` - Every month
|
|
||||||
RoleEnum:
|
|
||||||
enum:
|
|
||||||
- G
|
|
||||||
- H
|
|
||||||
- P
|
|
||||||
- A
|
|
||||||
type: string
|
|
||||||
description: |-
|
|
||||||
* `G` - Guest
|
|
||||||
* `H` - Host
|
|
||||||
* `P` - Manager
|
|
||||||
* `A` - Admin
|
|
||||||
Schedule:
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
@ -7182,6 +7203,40 @@ components:
|
||||||
- instance
|
- instance
|
||||||
- position
|
- position
|
||||||
- starts_at
|
- starts_at
|
||||||
|
RecordEnabledEnum:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
type: integer
|
||||||
|
description: |-
|
||||||
|
* `0` - No
|
||||||
|
* `1` - Yes
|
||||||
|
RepeatKindEnum:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
- 4
|
||||||
|
- 5
|
||||||
|
- 2
|
||||||
|
type: integer
|
||||||
|
description: |-
|
||||||
|
* `0` - Every week
|
||||||
|
* `1` - Every 2 weeks
|
||||||
|
* `4` - Every 3 weeks
|
||||||
|
* `5` - Every 4 weeks
|
||||||
|
* `2` - Every month
|
||||||
|
RoleEnum:
|
||||||
|
enum:
|
||||||
|
- G
|
||||||
|
- H
|
||||||
|
- P
|
||||||
|
- A
|
||||||
|
type: string
|
||||||
|
description: |-
|
||||||
|
* `G` - Guest
|
||||||
|
* `H` - Host
|
||||||
|
* `P` - Manager
|
||||||
|
* `A` - Admin
|
||||||
ServiceRegister:
|
ServiceRegister:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -7241,6 +7296,16 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
auto_playlist_repeat:
|
auto_playlist_repeat:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
intro_playlist:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
override_intro_playlist:
|
||||||
|
type: boolean
|
||||||
|
outro_playlist:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
override_outro_playlist:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- auto_playlist_enabled
|
- auto_playlist_enabled
|
||||||
- auto_playlist_repeat
|
- auto_playlist_repeat
|
||||||
|
@ -7249,6 +7314,8 @@ components:
|
||||||
- linked
|
- linked
|
||||||
- live_enabled
|
- live_enabled
|
||||||
- name
|
- name
|
||||||
|
- override_intro_playlist
|
||||||
|
- override_outro_playlist
|
||||||
ShowDays:
|
ShowDays:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -7535,10 +7602,19 @@ components:
|
||||||
message_offline:
|
message_offline:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
replay_gain_enabled:
|
||||||
|
type: boolean
|
||||||
|
readOnly: true
|
||||||
|
replay_gain_offset:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- input_fade_transition
|
- input_fade_transition
|
||||||
- message_format
|
- message_format
|
||||||
- message_offline
|
- message_offline
|
||||||
|
- replay_gain_enabled
|
||||||
|
- replay_gain_offset
|
||||||
StreamState:
|
StreamState:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -7764,6 +7840,66 @@ components:
|
||||||
* `4` - Friday
|
* `4` - Friday
|
||||||
* `5` - Saturday
|
* `5` - Saturday
|
||||||
* `6` - Sunday
|
* `6` - Sunday
|
||||||
|
WriteSchedule:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
readOnly: true
|
||||||
|
starts_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
ends_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
length:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
fade_in:
|
||||||
|
type: string
|
||||||
|
format: time
|
||||||
|
nullable: true
|
||||||
|
fade_out:
|
||||||
|
type: string
|
||||||
|
format: time
|
||||||
|
nullable: true
|
||||||
|
cue_in:
|
||||||
|
type: string
|
||||||
|
cue_out:
|
||||||
|
type: string
|
||||||
|
position:
|
||||||
|
type: integer
|
||||||
|
maximum: 2147483647
|
||||||
|
minimum: -2147483648
|
||||||
|
position_status:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/PositionStatusEnum"
|
||||||
|
minimum: -32768
|
||||||
|
maximum: 32767
|
||||||
|
broadcasted:
|
||||||
|
type: integer
|
||||||
|
maximum: 32767
|
||||||
|
minimum: -32768
|
||||||
|
played:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
instance:
|
||||||
|
type: integer
|
||||||
|
file:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
stream:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
required:
|
||||||
|
- broadcasted
|
||||||
|
- cue_in
|
||||||
|
- cue_out
|
||||||
|
- ends_at
|
||||||
|
- id
|
||||||
|
- instance
|
||||||
|
- position
|
||||||
|
- starts_at
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
basicAuth:
|
basicAuth:
|
||||||
type: http
|
type: http
|
||||||
|
|
18
api/setup.py
18
api/setup.py
|
@ -1,6 +1,6 @@
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
version = "4.0.0" # x-release-please-version
|
version = "4.2.0" # x-release-please-version
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="libretime-api",
|
name="libretime-api",
|
||||||
|
@ -26,22 +26,22 @@ setup(
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"django-cors-headers>=3.14.0,<4.4",
|
"django-cors-headers>=3.14.0,<4.5",
|
||||||
"django-filter>=2.4.0,<23.6",
|
"django-filter>=2.4.0,<24.4",
|
||||||
"django>=4.2.0,<4.3",
|
"django>=4.2.0,<4.3",
|
||||||
"djangorestframework>=3.14.0,<3.15",
|
"djangorestframework>=3.14.0,<3.16",
|
||||||
"drf-spectacular>=0.22.1,<0.28",
|
"drf-spectacular>=0.22.1,<0.29",
|
||||||
"requests>=2.31.0,<2.32",
|
"requests>=2.32.2,<2.33",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
"prod": [
|
"prod": [
|
||||||
"gunicorn>=20.1.0,<21.3",
|
"gunicorn>=22.0.0,<23.1",
|
||||||
"psycopg[c]>=3.1.8,<3.2",
|
"psycopg[c]>=3.1.8,<3.2",
|
||||||
"uvicorn[standard]>=0.17.6,<0.26.0",
|
"uvicorn[standard]>=0.17.6,<0.33.0",
|
||||||
],
|
],
|
||||||
"dev": [
|
"dev": [
|
||||||
"django-coverage-plugin>=3.0.0,<4",
|
"django-coverage-plugin>=3.0.0,<4",
|
||||||
"django-stubs>=1.14.0,<5",
|
"django-stubs>=5.1.0,<6",
|
||||||
"djangorestframework-stubs>=1.8.0,<4",
|
"djangorestframework-stubs>=1.8.0,<4",
|
||||||
"model_bakery>=1.10.1,<2",
|
"model_bakery>=1.10.1,<2",
|
||||||
"psycopg[binary]>=3.1.8,<4",
|
"psycopg[binary]>=3.1.8,<4",
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
# This file is used for development. It it not intended for production!
|
# This file is used for development. It it not intended for production!
|
||||||
# See https://libretime.org/docs/developer-manual/development/environment/#docker-compose
|
# See https://libretime.org/docs/developer-manual/development/environment/#docker-compose
|
||||||
#
|
#
|
||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
@ -12,13 +10,13 @@ services:
|
||||||
test: pg_isready -U libretime
|
test: pg_isready -U libretime
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:3.12-alpine
|
image: rabbitmq:3.13-alpine
|
||||||
environment:
|
environment:
|
||||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_DEFAULT_VHOST:-/libretime}
|
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_DEFAULT_VHOST:-/libretime}
|
||||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-libretime}
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-libretime}
|
||||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-libretime} # Change me !
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-libretime} # Change me !
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: rabbitmq-diagnostics -q ping
|
test: nc -z 127.0.0.1 5672
|
||||||
|
|
||||||
playout:
|
playout:
|
||||||
image: ghcr.io/libretime/libretime-playout:${LIBRETIME_VERSION:-latest}
|
image: ghcr.io/libretime/libretime-playout:${LIBRETIME_VERSION:-latest}
|
||||||
|
|
|
@ -50,7 +50,7 @@ check them against pam.
|
||||||
|
|
||||||
The above configuration expects a PAM configuration for the `http-libretime` service.
|
The above configuration expects a PAM configuration for the `http-libretime` service.
|
||||||
|
|
||||||
To confiure this you need to create the file `/etc/pam.d/http-libretime` with the following contents.
|
To configure this you need to create the file `/etc/pam.d/http-libretime` with the following contents.
|
||||||
|
|
||||||
```
|
```
|
||||||
auth required pam_sss.so
|
auth required pam_sss.so
|
||||||
|
@ -113,3 +113,68 @@ general:
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now be able to use your FreeIPA credentials to log in to LibreTime.
|
You should now be able to use your FreeIPA credentials to log in to LibreTime.
|
||||||
|
|
||||||
|
## Setup Header Authentication
|
||||||
|
|
||||||
|
If you have an SSO system that supports trusted SSO header authentication such as [Authelia](https://www.authelia.com/),
|
||||||
|
you can configure LibreTime to login users based on those trusted headers.
|
||||||
|
|
||||||
|
This allows users to only need to log in once on the SSO system and not need to log in again. It also allows LibreTime
|
||||||
|
to indirectly support other authentication mechanisms such as OAuth2.
|
||||||
|
|
||||||
|
This ONLY affects Legacy/Legacy API auth and does NOT affect API V2 auth.
|
||||||
|
|
||||||
|
### Configure Headers
|
||||||
|
|
||||||
|
LibreTime needs to know what headers are sent, and what information is available to it. You can also
|
||||||
|
setup a predefined group mapping so users are automatically granted the desired permissions.
|
||||||
|
|
||||||
|
This configuration is in `/etc/libretime/config.yml`. The following is an example configuration for an SSO service
|
||||||
|
that does the following:
|
||||||
|
|
||||||
|
- Sends the username in the `Remote-User` HTTP header.
|
||||||
|
- Sends the email in the `Remote-Email` HTTP header.
|
||||||
|
- Sends the name in the `Remote-Name` HTTP header. Example `John Doe`
|
||||||
|
- Sends the comma delimited groups in the `Remote-Groups` HTTP header. Example `group 1,lt-admin,group2`
|
||||||
|
- Has an IP of `10.0.0.34` (not required). When not provided it is not checked.
|
||||||
|
- Users with the `lt-host` group should get host privileges.
|
||||||
|
- Users with the `lt-admin` group should get admin privileges.
|
||||||
|
- Users with the `lt-pm` group should get program manager privileges.
|
||||||
|
- Users with the `lt-superadmin` group should get super admin privileges.
|
||||||
|
- All other users should get guest privileges.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
header_auth:
|
||||||
|
user_header: Remote-User # This is the default and could be omitted
|
||||||
|
groups_header: Remote-Groups # This is the default and could be omitted
|
||||||
|
email_header: Remote-Email # This is the default and could be omitted
|
||||||
|
name_header: Remote-Name # This is the default and could be omitted
|
||||||
|
proxy_ip: 10.0.0.34
|
||||||
|
group_map:
|
||||||
|
host: lt-host
|
||||||
|
program_manager: lt-pm
|
||||||
|
admin: lt-admin
|
||||||
|
superadmin: lt-superadmin
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `user_header` is not found in the request, users will be kicked to the login page
|
||||||
|
with a message that their username/password is invalid and will not be able to log in. When `proxy_ip` is provided
|
||||||
|
it will check that the request is coming from the correct proxy before doing the login. This prevents users who have
|
||||||
|
internal network access from being able to login as whoever they want in LibreTime.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
|
||||||
|
If `proxy_ip` is not provided any user on the internal network can log in as any user in LibreTime.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Enable Header authentication
|
||||||
|
|
||||||
|
After everything is set up properly you can enable header auth in `config.yml`:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
general:
|
||||||
|
auth: LibreTime_Auth_Adaptor_Header
|
||||||
|
```
|
||||||
|
|
||||||
|
You should now be automatically logged into LibreTime when you click the `Login` button.
|
||||||
|
|
|
@ -22,7 +22,7 @@ First, set the version you want to install:
|
||||||
echo LIBRETIME_VERSION="{vars.version}" > .env
|
echo LIBRETIME_VERSION="{vars.version}" > .env
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
|
|
||||||
Download the docker-compose files from the repository:
|
Download the docker compose files from the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Load LIBRETIME_VERSION variable
|
# Load LIBRETIME_VERSION variable
|
||||||
|
@ -106,16 +106,16 @@ You can find more details in the `docker-compose.yml` file or on the external se
|
||||||
Next, run the following commands to setup the database:
|
Next, run the following commands to setup the database:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose run --rm api libretime-api migrate
|
docker compose run --rm api libretime-api migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, start the services, and check that they're running using the following commands:
|
Finally, start the services, and check that they're running using the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
docker-compose ps
|
docker compose ps
|
||||||
docker-compose logs -f
|
docker compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
## Securing LibreTime
|
## Securing LibreTime
|
||||||
|
|
|
@ -230,6 +230,9 @@ server {
|
||||||
|
|
||||||
server_name libretime.example.org;
|
server_name libretime.example.org;
|
||||||
|
|
||||||
|
client_max_body_size 512M;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
|
@ -76,6 +76,9 @@ server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name libretime.example.org;
|
server_name libretime.example.org;
|
||||||
|
|
||||||
|
client_max_body_size 512M;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
@ -124,6 +127,9 @@ server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name libretime.example.org;
|
server_name libretime.example.org;
|
||||||
|
|
||||||
|
client_max_body_size 512M;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
|
||||||
if ($host = libretime.example.org) {
|
if ($host = libretime.example.org) {
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
} # managed by Certbot
|
} # managed by Certbot
|
||||||
|
@ -135,6 +141,9 @@ server {
|
||||||
listen 443 ssl; # managed by Certbot
|
listen 443 ssl; # managed by Certbot
|
||||||
server_name libretime.example.org;
|
server_name libretime.example.org;
|
||||||
|
|
||||||
|
client_max_body_size 512M;
|
||||||
|
client_body_timeout 300s;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/libretime.example.org/fullchain.pem; # managed by Certbot
|
ssl_certificate /etc/letsencrypt/live/libretime.example.org/fullchain.pem; # managed by Certbot
|
||||||
ssl_certificate_key /etc/letsencrypt/live/libretime.example.org/privkey.pem; # managed by Certbot
|
ssl_certificate_key /etc/letsencrypt/live/libretime.example.org/privkey.pem; # managed by Certbot
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
|
|
@ -17,6 +17,12 @@ Setting a higher bitrate for your output stream will only benefit your listeners
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
The liquidsoap playout handler version 1.4.3 shipped in Debian Bullseye and 1.4.1 shipped in Ubuntu Focal doesn't support AAC streaming output. If you want to stream AAC, you will need to replace the liquidsoap package with a version that supports AAC. See this [tutorial](./tutorials/setup-liquidsoap-aac-streaming.md) for more information.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
## Icecast
|
## Icecast
|
||||||
|
|
||||||
### UTF-8 metadata in Icecast MP3 streams
|
### UTF-8 metadata in Icecast MP3 streams
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
title: How to update liquidsoap to support AAC streaming
|
||||||
|
---
|
||||||
|
|
||||||
|
This tutorials walks you though the steps required to replace the liquidsoap package with a version that supports AAC streaming.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
Replacing the liquidsoap package has security implications, since this will remove the package from the system's package manager. This means that the package manager will not be able to update the liquidsoap package in the future. This includes backports of security fixes.
|
||||||
|
|
||||||
|
Libretime is NOT compatible with Liquidsoap 2.x at the time of this writing. Future versions of Libretime will support Liquidsoap 2.x which will render these instructions obsolete.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::info
|
||||||
|
|
||||||
|
Lets assume you already [installed LibreTime using the native OS installer](../install/install-using-the-installer.md). Execute the following commands as the libretime user.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 1. Obtain liquidsoap with AAC support
|
||||||
|
|
||||||
|
For Ubuntu 20.04 LTS ('focal'), use the following file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://github.com/savonet/liquidsoap/releases/download/v1.4.4/liquidsoap-v1.4.4_1.4.4-ubuntu-focal-amd64-1_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
For Debian 11 ('Bullseye'), first enable non-free package source for libfdk-aac support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install software-properties-common
|
||||||
|
sudo apt-add-repository -c non-free
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use the following file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://github.com/savonet/liquidsoap/releases/download/v1.4.4/liquidsoap-v1.4.4_1.4.4-debian-testing-amd64-1_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Install and replace the liquidsoap package
|
||||||
|
|
||||||
|
Install the package using `apt`, then remove the old liquidsoap dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt -y install ./liquidsoap-v1.4.4_1.4.4-*-amd64-1_amd64.deb
|
||||||
|
sudo apt -y autoremove
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Configure LibreTime to use the new liquidsoap package
|
||||||
|
|
||||||
|
Nothing to do, this is a drop-in replacement. Just restart the libretime target once and then check the status page in the LibreTime web interface to see if the liquidsoap service is running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart libretime.target
|
||||||
|
```
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
If you want to update LibreTime in the future, you'll need to re-run the installer schript. This will replace the liquidsoap package with the version that doesn't support AAC streaming. Add `--packages-excludes liquidsoap` to the installer command to prevent this from happening.
|
||||||
|
|
||||||
|
:::
|
|
@ -54,7 +54,7 @@ liable to legal action.
|
||||||
If you want to go down the commercial music route, check out the
|
If you want to go down the commercial music route, check out the
|
||||||
https://www.prsformusic.com and https://www.ppluk.com websites for UK licence
|
https://www.prsformusic.com and https://www.ppluk.com websites for UK licence
|
||||||
details. In the USA, the https://www.soundexchange.com website currently quotes
|
details. In the USA, the https://www.soundexchange.com website currently quotes
|
||||||
a 500 dollar minimum annual fee for non-commercial webcasters, plus a usage fee
|
a 1000 (per Jan. 2024) dollar minimum annual fee for non-commercial webcasters, plus a usage fee
|
||||||
above a certain number of listener hours, for the right to stream music
|
above a certain number of listener hours, for the right to stream music
|
||||||
recordings to listeners. See the websites of [ASCAP](https://www.ascap.com),
|
recordings to listeners. See the websites of [ASCAP](https://www.ascap.com),
|
||||||
[BMI](https://www.bmi.com) and [SESAC](https://www.sesac.com) for details of music
|
[BMI](https://www.bmi.com) and [SESAC](https://www.sesac.com) for details of music
|
||||||
|
|
|
@ -13,8 +13,8 @@ LibreTime development workflows follow the standardized [C4 development process]
|
||||||
- [2.4. Development Process](https://rfc.zeromq.org/spec/42/#24-development-process)
|
- [2.4. Development Process](https://rfc.zeromq.org/spec/42/#24-development-process)
|
||||||
- `16.` Maintainers MAY NOT merge incorrect patches.
|
- `16.` Maintainers MAY NOT merge incorrect patches.
|
||||||
- [2.5. Branches and Releases](https://rfc.zeromq.org/spec/42/#25-branches-and-releases)
|
- [2.5. Branches and Releases](https://rfc.zeromq.org/spec/42/#25-branches-and-releases)
|
||||||
- `1.` The project SHALL have a development branch (`main`) that always holds the latest in-progress version and SHOULD always build. The project MAY have a bug fixes only branch (`stable`) that always holds the current stable version and SHOULD always build.
|
- `1.` The project SHALL have a development branch (`main`) that always holds the latest in-progress version and SHOULD always build. The project MAY have a bug fixes only branch (`stable-*`) that always holds the current stable version and SHOULD always build.
|
||||||
- `3.` To make a stable release a Maintainer shall tag the repository. Stable releases SHALL always be released from the repository `main` or `stable` branches.
|
- `3.` To make a stable release a Maintainer shall tag the repository. Stable releases SHALL always be released from the repository `main` or `stable-*` branches.
|
||||||
|
|
||||||
## Contribute financially
|
## Contribute financially
|
||||||
|
|
||||||
|
|
|
@ -14,16 +14,16 @@ To setup a docker-compose development environment, run the following commands:
|
||||||
# Clean and build
|
# Clean and build
|
||||||
make clean
|
make clean
|
||||||
cp .env.dev .env
|
cp .env.dev .env
|
||||||
DOCKER_BUILDKIT=1 docker-compose build
|
DOCKER_BUILDKIT=1 docker compose build
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
make dev-certs
|
make dev-certs
|
||||||
docker-compose run --rm legacy make build
|
docker compose run --rm legacy make build
|
||||||
docker-compose run --rm api libretime-api migrate
|
docker compose run --rm api libretime-api migrate
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
docker-compose logs -f
|
docker compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
@ -33,7 +33,7 @@ You may also use the following `make clean dev` shortcut:
|
||||||
```bash
|
```bash
|
||||||
make clean dev
|
make clean dev
|
||||||
|
|
||||||
docker-compose logs -f
|
docker compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
|
@ -7,7 +7,7 @@ title: Development workflows
|
||||||
LibreTime uses [Github pull requests to manage changes](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). The workflow looks like this:
|
LibreTime uses [Github pull requests to manage changes](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). The workflow looks like this:
|
||||||
|
|
||||||
- [Create a fork of the project](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
|
- [Create a fork of the project](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
|
||||||
- Check out the `main` branch. If you're making a minor or small documentation change you can check out the `stable` branch.
|
- Check out the `main` branch.
|
||||||
- Create a new branch based on the checked out branch.
|
- Create a new branch based on the checked out branch.
|
||||||
- Work on your changes locally. Try to keep each commit small to make reviews easier.
|
- Work on your changes locally. Try to keep each commit small to make reviews easier.
|
||||||
- Lint and test the codebase, for example using the `make lint` or `make test` commands inside the app folder you want to check.
|
- Lint and test the codebase, for example using the `make lint` or `make test` commands inside the app folder you want to check.
|
||||||
|
|
|
@ -15,8 +15,8 @@ Once a release is desired, checkout the release branch:
|
||||||
```bash
|
```bash
|
||||||
# For a release on the main branch
|
# For a release on the main branch
|
||||||
git checkout release-please--branches--main--components--libretime
|
git checkout release-please--branches--main--components--libretime
|
||||||
# For a release on the stable branch
|
# For a release on the stable-4.x branch
|
||||||
git checkout release-please--branches--stable--components--libretime
|
git checkout release-please--branches--stable-4.x--components--libretime
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Release note
|
### 2. Release note
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
title: LibreTime 4.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
import ReleaseHead from './\_release-head.md';
|
||||||
|
|
||||||
|
<ReleaseHead date='2024-05-05' version='4.1.0'/>
|
||||||
|
|
||||||
|
## :sparkling_heart: Contributors
|
||||||
|
|
||||||
|
The LibreTime project wants to thank the following contributors for authoring PRs to this release:
|
||||||
|
|
||||||
|
- @caveman99
|
||||||
|
- @jooola
|
||||||
|
- @kmahelona
|
||||||
|
- @mp3butcher
|
||||||
|
- @paddatrapper
|
||||||
|
|
||||||
|
## :rocket: Features
|
||||||
|
|
||||||
|
Please see the [changelog](https://github.com/libretime/libretime/blob/main/CHANGELOG.md#410-2024-05-05).
|
||||||
|
|
||||||
|
## :bug: Bug fixes
|
||||||
|
|
||||||
|
Please see the [changelog](https://github.com/libretime/libretime/blob/main/CHANGELOG.md#410-2024-05-05).
|
||||||
|
|
||||||
|
## :arrow_up: Upgrading
|
||||||
|
|
||||||
|
### Replay gain modifier preference
|
||||||
|
|
||||||
|
The `replay_gain_modifier` preference is now stored as system preference. Please check and save the replay gain modifier preference manually to make sure the preference is up to date and usable.
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: LibreTime 4.2.0
|
||||||
|
---
|
||||||
|
|
||||||
|
import ReleaseHead from './\_release-head.md';
|
||||||
|
|
||||||
|
<ReleaseHead date='2024-06-22' version='4.2.0'/>
|
||||||
|
|
||||||
|
## :sparkling_heart: Contributors
|
||||||
|
|
||||||
|
The LibreTime project wants to thank the following contributors for authoring PRs to this release:
|
||||||
|
|
||||||
|
- @conet
|
||||||
|
- @dakriy
|
||||||
|
- @jooola
|
||||||
|
- @paddatrapper
|
||||||
|
- @rjhelms
|
||||||
|
|
||||||
|
## :rocket: Features
|
||||||
|
|
||||||
|
Please see the [changelog](https://github.com/libretime/libretime/blob/main/CHANGELOG.md#420-2024-06-22).
|
||||||
|
|
||||||
|
## :bug: Bug fixes
|
||||||
|
|
||||||
|
Please see the [changelog](https://github.com/libretime/libretime/blob/main/CHANGELOG.md#420-2024-06-22).
|
|
@ -30,3 +30,4 @@ New releases target the current stable distributions release, and development sh
|
||||||
| Versions | | | | |
|
| Versions | | | | |
|
||||||
| 3.0.x | deprecated | deprecated | recommended | recommended |
|
| 3.0.x | deprecated | deprecated | recommended | recommended |
|
||||||
| 3.1.x | | | recommended | recommended |
|
| 3.1.x | | | recommended | recommended |
|
||||||
|
| 4.0.x | | | recommended | recommended |
|
||||||
|
|
|
@ -12,8 +12,12 @@ The LibreTime project wants to thank the following contributors for authoring PR
|
||||||
|
|
||||||
## :rocket: Features
|
## :rocket: Features
|
||||||
|
|
||||||
|
Please see the [changelog](https://github.com/libretime/libretime/blob/main/CHANGELOG.md).
|
||||||
|
|
||||||
## :bug: Bug fixes
|
## :bug: Bug fixes
|
||||||
|
|
||||||
|
Please see the [changelog](https://github.com/libretime/libretime/blob/main/CHANGELOG.md).
|
||||||
|
|
||||||
## :fire: Deprecation and removal
|
## :fire: Deprecation and removal
|
||||||
|
|
||||||
## :arrow_up: Before upgrading
|
## :arrow_up: Before upgrading
|
||||||
|
|
|
@ -37,6 +37,7 @@ Smart blocks are automatically filled with media files from the LibreTime librar
|
||||||
To create a smart block, click the **Smartblocks** button on the left sidebar, and select **New** from the toolbar. Like a playlist, smart blocks can have a title and description, which you can edit. This helps you find relevant smart blocks in searches.
|
To create a smart block, click the **Smartblocks** button on the left sidebar, and select **New** from the toolbar. Like a playlist, smart blocks can have a title and description, which you can edit. This helps you find relevant smart blocks in searches.
|
||||||
|
|
||||||
Fill out the smart block's **Name**, **Search Criteria**, and **Limit to** sections. The search criteria can be any one of LibreTime's metadata categories, such as **Title**, **Creator** or **Genre**. The modifier depends on whether the metadata in question contains letters or numbers. For example, **Title** has modifiers including _contains_ and _starts with_, whereas the modifiers for **BPM** include _is greater than_ and _is in the range_.
|
Fill out the smart block's **Name**, **Search Criteria**, and **Limit to** sections. The search criteria can be any one of LibreTime's metadata categories, such as **Title**, **Creator** or **Genre**. The modifier depends on whether the metadata in question contains letters or numbers. For example, **Title** has modifiers including _contains_ and _starts with_, whereas the modifiers for **BPM** include _is greater than_ and _is in the range_.
|
||||||
|
To filter tracks using today's date information, use the `now{}` macro. Format characters are listed in the [php documentation](https://www.php.net/manual/en/datetime.format.php). For example, to filter to tracks with a **Title** that ends in `Instrumental Jan 2024` where `Jan 2024` is the current month and year, add a criteria for **Title** with a modifier of **ends with** and a value of `Instrumental now{M Y}`.
|
||||||
|
|
||||||
If you have a large number of files which meet the criteria that you specify, you may wish to limit the duration of the smart block using the **Limit to** field, so that it fits within the show you have in mind. Select **hours**, **minutes** or **items** from the drop-down menu, and click the **Generate** button again, if it's a static smart block. Then click the **Save** button.
|
If you have a large number of files which meet the criteria that you specify, you may wish to limit the duration of the smart block using the **Limit to** field, so that it fits within the show you have in mind. Select **hours**, **minutes** or **items** from the drop-down menu, and click the **Generate** button again, if it's a static smart block. Then click the **Save** button.
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,8 @@ indicator.
|
||||||
| Add Autoloading Playlist? | If checked, allows for the following options |
|
| Add Autoloading Playlist? | If checked, allows for the following options |
|
||||||
| Select Playlist | Select the playlist the show will autofill from (shows autofill exactly one hour before air). If you wish to use a smartblock you must add it to a playlist and then select that playlist. This can be used to auto schedule new podcast episodes to air. |
|
| Select Playlist | Select the playlist the show will autofill from (shows autofill exactly one hour before air). If you wish to use a smartblock you must add it to a playlist and then select that playlist. This can be used to auto schedule new podcast episodes to air. |
|
||||||
| Repeat Playlist Until Show Is Full | If checked, the playlist will be added to the show multiple times until the slot is full. Useful for applying a one-hour music playlist made up of smartblocks to a two-hour show. |
|
| Repeat Playlist Until Show Is Full | If checked, the playlist will be added to the show multiple times until the slot is full. Useful for applying a one-hour music playlist made up of smartblocks to a two-hour show. |
|
||||||
|
| Select Intro Playlist | Select the playlist to replace the global intro playlist from settings. If you wish to use a smartblock you must add it to a playlist and then select that playlist. |
|
||||||
|
| Select Outro Playlist | Select the playlist to replace the global outro playlist from settings. If you wish to use a smartblock you must add it to a playlist and then select that playlist. |
|
||||||
| _Live Stream Input_ | |
|
| _Live Stream Input_ | |
|
||||||
| Use LibreTime/Custom Authentication | |
|
| Use LibreTime/Custom Authentication | |
|
||||||
| Show Source | |
|
| Show Source | |
|
||||||
|
|
|
@ -21,6 +21,12 @@ time for the convenience of your station staff. You can also set the day of the
|
||||||
week that you wish to start your station's weekly schedule on, which defaults
|
week that you wish to start your station's weekly schedule on, which defaults
|
||||||
to Sunday.
|
to Sunday.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
The **Station Timezone** setting can not be modified on this page. It is set in the [configuration file](../admin-manual/configuration.md#general).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
The **Track Type Default** enables you to select a track type default for uploads.
|
The **Track Type Default** enables you to select a track type default for uploads.
|
||||||
|
|
||||||
Initially, the **Default Fade In** and **Default Fade Out** times for automated
|
Initially, the **Default Fade In** and **Default Fade Out** times for automated
|
||||||
|
@ -60,15 +66,20 @@ wish. (There is more about this feature in the
|
||||||
[_Exporting the schedule_](./playout-history.md) chapter, in the
|
[_Exporting the schedule_](./playout-history.md) chapter, in the
|
||||||
_Advanced Configuration_ section of this book).
|
_Advanced Configuration_ section of this book).
|
||||||
|
|
||||||
The **Allowed CORS URLs** is intended to deal with situations where you want a
|
:::note
|
||||||
remote site with a different domain to access the API. This is relevant when
|
|
||||||
there is a reverse proxy server in front of LibreTime. If you are using a
|
The **Allowed CORS URLs** you can still see in this screenshot was moved to the [configuration file](../admin-manual/configuration.md#general).
|
||||||
reverse proxy, the URLs that will be used to access it should be added here.
|
|
||||||
|
:::
|
||||||
|
|
||||||
The **Display login button on your Radio Page?** will determine whether visitors
|
The **Display login button on your Radio Page?** will determine whether visitors
|
||||||
to your site see a link to login. If this is disabled, DJs and admins will need
|
to your site see a link to login. If this is disabled, DJs and admins will need
|
||||||
to goto http://example.org/login to be able to login.
|
to goto http://example.org/login to be able to login.
|
||||||
|
|
||||||
|
The **Disable the public radio page and redirect to the login page?** will
|
||||||
|
switch off the public radio page and redirect all visitors to the login page.
|
||||||
|
This is useful if you want to use LibreTime as a backend for a custom website.
|
||||||
|
|
||||||
The **Tune-In Settings** section is intended for stations that have partnered
|
The **Tune-In Settings** section is intended for stations that have partnered
|
||||||
with TuneIn to automatically push their now playing metadata to TuneIn. This
|
with TuneIn to automatically push their now playing metadata to TuneIn. This
|
||||||
hasn't been tested and also requires special credentials from TuneIn.
|
hasn't been tested and also requires special credentials from TuneIn.
|
||||||
|
|
3
install
3
install
|
@ -605,7 +605,7 @@ info "creating python3 venv"
|
||||||
python3 -m venv "$VENV_DIR"
|
python3 -m venv "$VENV_DIR"
|
||||||
|
|
||||||
info "upgrading python3 tools"
|
info "upgrading python3 tools"
|
||||||
$VENV_DIR/bin/pip install --upgrade pip setuptools wheel
|
$VENV_DIR/bin/pip install --upgrade "pip<24.1" setuptools wheel
|
||||||
|
|
||||||
# Install Shared and API client
|
# Install Shared and API client
|
||||||
########################################################################################
|
########################################################################################
|
||||||
|
@ -683,6 +683,7 @@ install_service "libretime-analyzer.service" "$SCRIPT_DIR/analyzer/install/syste
|
||||||
section "Worker"
|
section "Worker"
|
||||||
|
|
||||||
install_python_app "$SCRIPT_DIR/worker"
|
install_python_app "$SCRIPT_DIR/worker"
|
||||||
|
link_python_app libretime-worker
|
||||||
|
|
||||||
info "creating libretime-worker working directory"
|
info "creating libretime-worker working directory"
|
||||||
mkdir_and_chown "$LIBRETIME_USER" "$WORKING_DIR/worker"
|
mkdir_and_chown "$LIBRETIME_USER" "$WORKING_DIR/worker"
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"css/media_library.css": "e1982d1f673543f7730898fb49450f8b",
|
"css/media_library.css": "e1982d1f673543f7730898fb49450f8b",
|
||||||
"css/player-form.css": "e08a4545715fc56b75c845b44a5b2a1c",
|
"css/player-form.css": "e08a4545715fc56b75c845b44a5b2a1c",
|
||||||
"css/player.css": "904bc7aede4d5f0372468528d88094f1",
|
"css/player.css": "904bc7aede4d5f0372468528d88094f1",
|
||||||
"css/playlist_builder.css": "e92ef56ddffca440a7741934edbb7f7f",
|
"css/playlist_builder.css": "9e35f1b7a1e79a7a73e7e9666d5a711f",
|
||||||
"css/playouthistory.css": "983cc1bac566b18b745b6e0da9ef3c0c",
|
"css/playouthistory.css": "983cc1bac566b18b745b6e0da9ef3c0c",
|
||||||
"css/plupload.queue.css": "0acfb6b54c18654452727d4abf297394",
|
"css/plupload.queue.css": "0acfb6b54c18654452727d4abf297394",
|
||||||
"css/pro_dropdown_3.css": "9848a27dad960c2218751c1656e9206a",
|
"css/pro_dropdown_3.css": "9848a27dad960c2218751c1656e9206a",
|
||||||
|
@ -43,13 +43,13 @@
|
||||||
"css/show_analytics.css": "4393c521308277447afabe8791779bf1",
|
"css/show_analytics.css": "4393c521308277447afabe8791779bf1",
|
||||||
"css/showbuilder.css": "4421c01b5c2dfb03f8d06dd6023b4bd7",
|
"css/showbuilder.css": "4421c01b5c2dfb03f8d06dd6023b4bd7",
|
||||||
"css/station_podcast.css": "88e9b38ead71eddc69ef50bfc8cb2d0d",
|
"css/station_podcast.css": "88e9b38ead71eddc69ef50bfc8cb2d0d",
|
||||||
"css/styles.css": "6890a553402f44cefc7c6915f38aa657",
|
"css/styles.css": "f38f7f24c3895c146babbb56a6534730",
|
||||||
"css/tipsy/jquery.tipsy.css": "b13517583583f83ed7d5fc067a0c9372",
|
"css/tipsy/jquery.tipsy.css": "b13517583583f83ed7d5fc067a0c9372",
|
||||||
"css/tracktypes.css": "94c94817a8505ff4dfcd090987859a7e",
|
"css/tracktypes.css": "94c94817a8505ff4dfcd090987859a7e",
|
||||||
"css/users.css": "94c94817a8505ff4dfcd090987859a7e",
|
"css/users.css": "94c94817a8505ff4dfcd090987859a7e",
|
||||||
"css/waveform.css": "4ce429708933e6da1a2f3bdb2a01db52",
|
"css/waveform.css": "4ce429708933e6da1a2f3bdb2a01db52",
|
||||||
"js/airtime/airtime_bootstrap.js": "9575982385f6c74e2b4ec61e30214a7c",
|
"js/airtime/airtime_bootstrap.js": "9575982385f6c74e2b4ec61e30214a7c",
|
||||||
"js/airtime/audiopreview/preview_jplayer.js": "133b4b9a3436716a8367d353f1658da3",
|
"js/airtime/audiopreview/preview_jplayer.js": "d3402345279a9f4b86b381bae87d6de8",
|
||||||
"js/airtime/buttons/buttons.js": "1a984b1e01262816c899c5fa3f12e3cd",
|
"js/airtime/buttons/buttons.js": "1a984b1e01262816c899c5fa3f12e3cd",
|
||||||
"js/airtime/common/audioplaytest.js": "93737dabc4cce4fcdcc6d54acf86d16d",
|
"js/airtime/common/audioplaytest.js": "93737dabc4cce4fcdcc6d54acf86d16d",
|
||||||
"js/airtime/common/common.js": "8c0675f5a1c8d95323b2f3983c658f62",
|
"js/airtime/common/common.js": "8c0675f5a1c8d95323b2f3983c658f62",
|
||||||
|
@ -59,11 +59,11 @@
|
||||||
"js/airtime/dashboard/versiontooltip.js": "53ed1c2f7dd9527cba80bbcdb239ac88",
|
"js/airtime/dashboard/versiontooltip.js": "53ed1c2f7dd9527cba80bbcdb239ac88",
|
||||||
"js/airtime/library/events/library_playlistbuilder.js": "7191ee58ad07b8f652be02bb131eb5e6",
|
"js/airtime/library/events/library_playlistbuilder.js": "7191ee58ad07b8f652be02bb131eb5e6",
|
||||||
"js/airtime/library/events/library_showbuilder.js": "f3d3f65fe1e7a80cd17c228889c6a1ae",
|
"js/airtime/library/events/library_showbuilder.js": "f3d3f65fe1e7a80cd17c228889c6a1ae",
|
||||||
"js/airtime/library/library.js": "bf61a213cf38521d1892034628faf17c",
|
"js/airtime/library/library.js": "869b3117dc2c119fac61775d78f63fa9",
|
||||||
"js/airtime/library/plupload.js": "0f6be5b133650828b9ffc74e7852dc89",
|
"js/airtime/library/plupload.js": "0f6be5b133650828b9ffc74e7852dc89",
|
||||||
"js/airtime/library/podcast.js": "4dedd84cb571cdba2401bfb8ba621e69",
|
"js/airtime/library/podcast.js": "4dedd84cb571cdba2401bfb8ba621e69",
|
||||||
"js/airtime/library/publish.js": "ab3a1452dd332cdb0773241a1c17b7e0",
|
"js/airtime/library/publish.js": "ab3a1452dd332cdb0773241a1c17b7e0",
|
||||||
"js/airtime/library/spl.js": "c4cbac0c237b548064685a2cb16d3fa2",
|
"js/airtime/library/spl.js": "5bddd886303ff15e8b78e79b30a9e56f",
|
||||||
"js/airtime/listenerstat/listenerstat.js": "a3733dae8f9549668125ec9852d356ed",
|
"js/airtime/listenerstat/listenerstat.js": "a3733dae8f9549668125ec9852d356ed",
|
||||||
"js/airtime/listenerstat/showlistenerstat.js": "7cf0c375420f1c8471d304bc8758b2cd",
|
"js/airtime/listenerstat/showlistenerstat.js": "7cf0c375420f1c8471d304bc8758b2cd",
|
||||||
"js/airtime/login/login.js": "7278cf49618791d75bacce38dd1b1d46",
|
"js/airtime/login/login.js": "7278cf49618791d75bacce38dd1b1d46",
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
"js/airtime/nowplaying/register.js": "7d1e5d38eee510c22e408077155ab672",
|
"js/airtime/nowplaying/register.js": "7d1e5d38eee510c22e408077155ab672",
|
||||||
"js/airtime/player/player.js": "76a78bfc1bac0c5479916d9d4641a5b1",
|
"js/airtime/player/player.js": "76a78bfc1bac0c5479916d9d4641a5b1",
|
||||||
"js/airtime/player/playerhtml5.js": "058b061891abf4b3ee827c8e83996611",
|
"js/airtime/player/playerhtml5.js": "058b061891abf4b3ee827c8e83996611",
|
||||||
"js/airtime/playlist/smart_blockbuilder.js": "36337e7e025d89976d754e34fc914887",
|
"js/airtime/playlist/smart_blockbuilder.js": "9e6bbad77150a589a6efd3b7da80a03b",
|
||||||
"js/airtime/playouthistory/configuretemplate.js": "44386f366d2baca3039f8c4cb9cd4422",
|
"js/airtime/playouthistory/configuretemplate.js": "44386f366d2baca3039f8c4cb9cd4422",
|
||||||
"js/airtime/playouthistory/historytable.js": "7e84ee76011ecf8f21abdd483487a45e",
|
"js/airtime/playouthistory/historytable.js": "7e84ee76011ecf8f21abdd483487a45e",
|
||||||
"js/airtime/playouthistory/template.js": "742af1a411aef1745fa2ad3082499452",
|
"js/airtime/playouthistory/template.js": "742af1a411aef1745fa2ad3082499452",
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
"js/airtime/showbuilder/main_builder.js": "d1ea3c3d834e47cd35c27efd75516190",
|
"js/airtime/showbuilder/main_builder.js": "d1ea3c3d834e47cd35c27efd75516190",
|
||||||
"js/airtime/showbuilder/tabs.js": "a0b5284afde950a6f32c7e9e75550690",
|
"js/airtime/showbuilder/tabs.js": "a0b5284afde950a6f32c7e9e75550690",
|
||||||
"js/airtime/status/status.js": "41456c6ed40f820e1e99215e81eb4bc4",
|
"js/airtime/status/status.js": "41456c6ed40f820e1e99215e81eb4bc4",
|
||||||
"js/airtime/tracktype/tracktype.js": "cca00731e212727908bec54f21d4be58",
|
"js/airtime/tracktype/tracktype.js": "1d11ec7ba6a79bc7a3cde1f167f4dc6e",
|
||||||
"js/airtime/user/user.js": "28811eb65c17bc9954900daeaed6c1ad",
|
"js/airtime/user/user.js": "28811eb65c17bc9954900daeaed6c1ad",
|
||||||
"js/airtime/utilities/utilities.js": "e5d83eea1b38d2ff19bb968d30af9c98",
|
"js/airtime/utilities/utilities.js": "e5d83eea1b38d2ff19bb968d30af9c98",
|
||||||
"js/airtime/widgets/table-example.js": "77d448b6496adf5be0a360640cd3980c",
|
"js/airtime/widgets/table-example.js": "77d448b6496adf5be0a360640cd3980c",
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
"js/jplayer/skin/jplayer.airtime.audio.preview.css": "c721fe0587569391dcfa7d0f7b440d2b",
|
"js/jplayer/skin/jplayer.airtime.audio.preview.css": "c721fe0587569391dcfa7d0f7b440d2b",
|
||||||
"js/jplayer/skin/jplayer.audio-preview.blue.monday.css": "8565bf8e077eeb5bbba3c88bed42a590",
|
"js/jplayer/skin/jplayer.audio-preview.blue.monday.css": "8565bf8e077eeb5bbba3c88bed42a590",
|
||||||
"js/jplayer/skin/jplayer.blue.monday.css": "491c7a82ae4571ae4a73645bf9755eaf",
|
"js/jplayer/skin/jplayer.blue.monday.css": "491c7a82ae4571ae4a73645bf9755eaf",
|
||||||
"js/libs/CSVexport.js": "07a91f824f41d2d0d1884b5dd10f63e4",
|
"js/libs/CSVexport.js": "42c1fcfff5c717482f21bff1a9002295",
|
||||||
"js/libs/angular.min.js": "26680517e1024ca2c4f9ed4e0aa1619e",
|
"js/libs/angular.min.js": "26680517e1024ca2c4f9ed4e0aa1619e",
|
||||||
"js/libs/dayjs.min.js": "bea3f1180a3e2e45eccf9d76f990f3b4",
|
"js/libs/dayjs.min.js": "bea3f1180a3e2e45eccf9d76f990f3b4",
|
||||||
"js/libs/dropzone.min.js": "f59ac59a89d9c569a27ca7ead38d2396",
|
"js/libs/dropzone.min.js": "f59ac59a89d9c569a27ca7ead38d2396",
|
||||||
|
@ -150,6 +150,7 @@
|
||||||
"js/plupload/i18n/it_IT.js": "d54d63ce65b652322b9411d545223a20",
|
"js/plupload/i18n/it_IT.js": "d54d63ce65b652322b9411d545223a20",
|
||||||
"js/plupload/i18n/ja_JP.js": "35ffcfa1681e2ffef02fe085ea041436",
|
"js/plupload/i18n/ja_JP.js": "35ffcfa1681e2ffef02fe085ea041436",
|
||||||
"js/plupload/i18n/ko_KR.js": "16f238d8f3bc779345f5e9d16a3471a2",
|
"js/plupload/i18n/ko_KR.js": "16f238d8f3bc779345f5e9d16a3471a2",
|
||||||
|
"js/plupload/i18n/nb_NO.js": "c23d8392d1f570b24a2bfe713e52d8b0",
|
||||||
"js/plupload/i18n/pl_PL.js": "5a32f95c5a79b846d3fc0388469beb40",
|
"js/plupload/i18n/pl_PL.js": "5a32f95c5a79b846d3fc0388469beb40",
|
||||||
"js/plupload/i18n/pt_BR.js": "049e0eade66ed92564e1468051e70167",
|
"js/plupload/i18n/pt_BR.js": "049e0eade66ed92564e1468051e70167",
|
||||||
"js/plupload/i18n/ru_RU.js": "a4ede8ec9bd8f1d703d55bc135453042",
|
"js/plupload/i18n/ru_RU.js": "a4ede8ec9bd8f1d703d55bc135453042",
|
||||||
|
@ -180,5 +181,13 @@
|
||||||
"js/waveformplaylist/playout.js": "7dfc5fe760f3c6739e38499df7b61e47",
|
"js/waveformplaylist/playout.js": "7dfc5fe760f3c6739e38499df7b61e47",
|
||||||
"js/waveformplaylist/time_scale.js": "74e0e17e1c8cd597449220c98de408ba",
|
"js/waveformplaylist/time_scale.js": "74e0e17e1c8cd597449220c98de408ba",
|
||||||
"js/waveformplaylist/track.js": "5456e6081ffedf55a9e38571bc178781",
|
"js/waveformplaylist/track.js": "5456e6081ffedf55a9e38571bc178781",
|
||||||
"js/waveformplaylist/track_render.js": "e371b582b23e4b618e039f096d2f0570"
|
"js/wavesurfer/cursor.js": "8ed17a7437f3ec84972d15d0073249b2",
|
||||||
|
"js/wavesurfer/cursor.min.js": "831165862b629e615cf59112fa00d963",
|
||||||
|
"js/wavesurfer/libretime.js": "dfaefa6d32657faa77af6145d9d191ae",
|
||||||
|
"js/wavesurfer/regions.js": "aafe4f696d3da50c976d11e472fd56d1",
|
||||||
|
"js/wavesurfer/regions.min.js": "2ed2f8b5880beee568942000a6139e85",
|
||||||
|
"js/wavesurfer/timeline.js": "0bd70779070513c2a4f34237a0f9f573",
|
||||||
|
"js/wavesurfer/timeline.min.js": "90ea16b23cacebfad10cad42f94403d0",
|
||||||
|
"js/wavesurfer/wavesurfer.js": "9e2ced8a136449f4fd78911b0f01f6ed",
|
||||||
|
"js/wavesurfer/wavesurfer.min.js": "42ebd7fdd574dfe8cae587145751a1f2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth adaptor for basic header authentication.
|
||||||
|
*/
|
||||||
|
class LibreTime_Auth_Adaptor_Header implements Zend_Auth_Adapter_Interface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function authenticate(): Zend_Auth_Result
|
||||||
|
{
|
||||||
|
$trustedIp = Config::get('header_auth.proxy_ip');
|
||||||
|
if ($trustedIp != null && $_SERVER['REMOTE_ADDR'] != trim($trustedIp)) {
|
||||||
|
return new Zend_Auth_Result(Zend_Auth_Result::FAILURE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userHeader = Config::get('header_auth.user_header');
|
||||||
|
$groupsHeader = Config::get('header_auth.groups_header');
|
||||||
|
$emailHeader = Config::get('header_auth.email_header');
|
||||||
|
$nameHeader = Config::get('header_auth.name_header');
|
||||||
|
|
||||||
|
$userLogin = $this->getHeaderValueOf($userHeader);
|
||||||
|
|
||||||
|
if ($userLogin == null) {
|
||||||
|
return new Zend_Auth_Result(Zend_Auth_Result::FAILURE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$subj = CcSubjsQuery::create()->findOneByDbLogin($userLogin);
|
||||||
|
|
||||||
|
if ($subj == null) {
|
||||||
|
$user = new Application_Model_User('');
|
||||||
|
$user->setPassword('');
|
||||||
|
$user->setLogin($userLogin);
|
||||||
|
} else {
|
||||||
|
$user = new Application_Model_User($subj->getDbId());
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->getHeaderValueOf($nameHeader);
|
||||||
|
|
||||||
|
$user->setEmail($this->getHeaderValueOf($emailHeader));
|
||||||
|
$user->setFirstName($this->getFirstName($name) ?? '');
|
||||||
|
$user->setLastName($this->getLastName($name) ?? '');
|
||||||
|
$user->setType($this->getUserType($this->getHeaderValueOf($groupsHeader)));
|
||||||
|
$user->save();
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUserType(?string $groups): string
|
||||||
|
{
|
||||||
|
if ($groups == null) {
|
||||||
|
return UTYPE_GUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = array_map(fn ($group) => trim($group), explode(',', $groups));
|
||||||
|
|
||||||
|
$superAdminGroup = Config::get('header_auth.group_map.superadmin');
|
||||||
|
if (in_array($superAdminGroup, $groups)) {
|
||||||
|
return UTYPE_SUPERADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminGroup = Config::get('header_auth.group_map.admin');
|
||||||
|
if (in_array($adminGroup, $groups)) {
|
||||||
|
return UTYPE_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
$programManagerGroup = Config::get('header_auth.group_map.program_manager');
|
||||||
|
if (in_array($programManagerGroup, $groups)) {
|
||||||
|
return UTYPE_PROGRAM_MANAGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hostGroup = Config::get('header_auth.group_map.host');
|
||||||
|
if (in_array($hostGroup, $groups)) {
|
||||||
|
return UTYPE_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
return UTYPE_GUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFirstName(?string $name): ?string
|
||||||
|
{
|
||||||
|
if ($name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = explode(' ', $name, 2);
|
||||||
|
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLastName(?string $name): ?string
|
||||||
|
{
|
||||||
|
if ($name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = explode(' ', $name, 2);
|
||||||
|
|
||||||
|
return end($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHeaderValueOf(string $httpHeader): ?string
|
||||||
|
{
|
||||||
|
// Normalize the header name to match server's format
|
||||||
|
$normalizedHeader = 'HTTP_' . strtoupper(str_replace('-', '_', $httpHeader));
|
||||||
|
|
||||||
|
return $_SERVER[$normalizedHeader] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needed for zend auth adapter
|
||||||
|
|
||||||
|
private Application_Model_User $user;
|
||||||
|
|
||||||
|
public function setIdentity($username)
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCredential($password)
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return dummy object for internal auth handling.
|
||||||
|
*
|
||||||
|
* we need to build a dummpy object since the auth layer knows nothing about the db
|
||||||
|
*
|
||||||
|
* @param null $returnColumns
|
||||||
|
* @param null $omitColumns
|
||||||
|
*
|
||||||
|
* @return stdClass
|
||||||
|
*/
|
||||||
|
public function getResultRowObject($returnColumns = null, $omitColumns = null)
|
||||||
|
{
|
||||||
|
$o = new stdClass();
|
||||||
|
$o->id = $this->user->getId();
|
||||||
|
$o->username = $this->user->getLogin();
|
||||||
|
$o->password = $this->user->getPassword();
|
||||||
|
$o->real_name = implode(' ', [$this->user->getFirstName(), $this->user->getLastName()]);
|
||||||
|
$o->type = $this->user->getType();
|
||||||
|
$o->login = $this->user->getLogin();
|
||||||
|
|
||||||
|
return $o;
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,8 +33,17 @@ class AutoPlaylistManager
|
||||||
// call the addPlaylist to show function and don't check for user permission to avoid call to non-existant user object
|
// call the addPlaylist to show function and don't check for user permission to avoid call to non-existant user object
|
||||||
$sid = $si->getShowId();
|
$sid = $si->getShowId();
|
||||||
$playlistrepeat = new Application_Model_Show($sid);
|
$playlistrepeat = new Application_Model_Show($sid);
|
||||||
|
if ($playlistrepeat->getHasOverrideIntroPlaylist()) {
|
||||||
|
$introplaylistid = $playlistrepeat->getIntroPlaylistId();
|
||||||
|
} else {
|
||||||
$introplaylistid = Application_Model_Preference::GetIntroPlaylist();
|
$introplaylistid = Application_Model_Preference::GetIntroPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($playlistrepeat->getHasOverrideOutroPlaylist()) {
|
||||||
|
$outroplaylistid = $playlistrepeat->getOutroPlaylistId();
|
||||||
|
} else {
|
||||||
$outroplaylistid = Application_Model_Preference::GetOutroPlaylist();
|
$outroplaylistid = Application_Model_Preference::GetOutroPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
// we want to check and see if we need to repeat this process until the show is 100% scheduled
|
// we want to check and see if we need to repeat this process until the show is 100% scheduled
|
||||||
// so we create a while loop and break it immediately if repeat until full isn't enabled
|
// so we create a while loop and break it immediately if repeat until full isn't enabled
|
||||||
|
@ -81,6 +90,11 @@ class AutoPlaylistManager
|
||||||
$si->addPlaylistToShow($outroplaylistid, false);
|
$si->addPlaylistToShow($outroplaylistid, false);
|
||||||
}
|
}
|
||||||
$si->setAutoPlaylistBuilt(true);
|
$si->setAutoPlaylistBuilt(true);
|
||||||
|
|
||||||
|
// now trim excessively overbooked shows so the display isn't cluttered with myriads of red off-time blocks
|
||||||
|
if (Application_Model_Preference::getScheduleTrimOverbooked()) {
|
||||||
|
$si->trimOverbooked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Application_Model_Preference::setAutoPlaylistPollLock(microtime(true));
|
Application_Model_Preference::setAutoPlaylistPollLock(microtime(true));
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ class ZendActionHttpException extends Exception
|
||||||
$statusCode,
|
$statusCode,
|
||||||
$message,
|
$message,
|
||||||
$code = 0,
|
$code = 0,
|
||||||
Exception $previous = null
|
?Exception $previous = null
|
||||||
) {
|
) {
|
||||||
Logging::error('Error in action ' . $action->getRequest()->getActionName()
|
Logging::error('Error in action ' . $action->getRequest()->getActionName()
|
||||||
. " with status code {$statusCode}: {$message}");
|
. " with status code {$statusCode}: {$message}");
|
||||||
|
|
|
@ -98,6 +98,7 @@ class Application_Common_LocaleHelper
|
||||||
'mt' => _('Maltese'),
|
'mt' => _('Maltese'),
|
||||||
'my' => _('Burmese'),
|
'my' => _('Burmese'),
|
||||||
'na' => _('Nauru'),
|
'na' => _('Nauru'),
|
||||||
|
'nb' => _('Norwegian Bokmål'),
|
||||||
'ne' => _('Nepali'),
|
'ne' => _('Nepali'),
|
||||||
'nl' => _('Dutch'),
|
'nl' => _('Dutch'),
|
||||||
'no' => _('Norwegian'),
|
'no' => _('Norwegian'),
|
||||||
|
|
|
@ -99,6 +99,22 @@ class Schema implements ConfigurationInterface
|
||||||
/**/->scalarNode('filter_field')->end()
|
/**/->scalarNode('filter_field')->end()
|
||||||
->end()->end()
|
->end()->end()
|
||||||
|
|
||||||
|
// Header Auth Schema
|
||||||
|
->arrayNode('header_auth')->children()
|
||||||
|
/**/->scalarNode('user_header')->defaultValue('Remote-User')->end()
|
||||||
|
/**/->scalarNode('groups_header')->defaultValue('Remote-Groups')->end()
|
||||||
|
/**/->scalarNode('email_header')->defaultValue('Remote-Email')->end()
|
||||||
|
/**/->scalarNode('name_header')->defaultValue('Remote-Name')->end()
|
||||||
|
/**/->scalarNode('proxy_ip')->end()
|
||||||
|
/**/->scalarNode('locale')->defaultValue('en-US')->end()
|
||||||
|
/**/->arrayNode('group_map')->children()
|
||||||
|
/* */->scalarNode('host')->end()
|
||||||
|
/* */->scalarNode('program_manager')->end()
|
||||||
|
/* */->scalarNode('admin')->end()
|
||||||
|
/* */->scalarNode('superadmin')->end()
|
||||||
|
/**/->end()->end()
|
||||||
|
->end()->end()
|
||||||
|
|
||||||
// Playout schema
|
// Playout schema
|
||||||
->arrayNode('playout')
|
->arrayNode('playout')
|
||||||
/**/->ignoreExtraKeys()
|
/**/->ignoreExtraKeys()
|
||||||
|
|
|
@ -8,6 +8,12 @@ class IndexController extends Zend_Controller_Action
|
||||||
{
|
{
|
||||||
$CC_CONFIG = Config::getConfig();
|
$CC_CONFIG = Config::getConfig();
|
||||||
$baseUrl = Config::getBasePath();
|
$baseUrl = Config::getBasePath();
|
||||||
|
if (Application_Model_Preference::getRadioPageDisabled()) {
|
||||||
|
$this->_helper->redirector->gotoUrl($baseUrl . 'login');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->view->headTitle(Application_Model_Preference::GetHeadTitle());
|
$this->view->headTitle(Application_Model_Preference::GetHeadTitle());
|
||||||
$this->view->headScript()->appendFile(Assets::url('js/libs/jquery-1.8.3.min.js'), 'text/javascript');
|
$this->view->headScript()->appendFile(Assets::url('js/libs/jquery-1.8.3.min.js'), 'text/javascript');
|
||||||
|
|
||||||
|
|
|
@ -398,8 +398,35 @@ class LibraryController extends Zend_Controller_Action
|
||||||
$this->view->id = $file_id;
|
$this->view->id = $file_id;
|
||||||
$this->view->title = $file->getPropelOrm()->getDbTrackTitle();
|
$this->view->title = $file->getPropelOrm()->getDbTrackTitle();
|
||||||
$this->view->artist_name = $file->getPropelOrm()->getDbArtistName();
|
$this->view->artist_name = $file->getPropelOrm()->getDbArtistName();
|
||||||
$this->view->filePath = $file->getPropelOrm()->getDbFilepath();
|
|
||||||
$this->view->artwork = $file->getPropelOrm()->getDbArtwork();
|
$this->view->artwork = $file->getPropelOrm()->getDbArtwork();
|
||||||
|
$this->view->replay_gain = $file->getPropelOrm()->getDbReplayGain();
|
||||||
|
$this->view->cuein = $file->getPropelOrm()->getDbCuein();
|
||||||
|
$this->view->cueout = $file->getPropelOrm()->getDbCueout();
|
||||||
|
$this->view->format = $file->getPropelOrm()->getDbFormat();
|
||||||
|
$this->view->bit_rate = $file->getPropelOrm()->getDbBitRate();
|
||||||
|
$this->view->sample_rate = $file->getPropelOrm()->getDbSampleRate();
|
||||||
|
$filePath = $file->getPropelOrm()->getDbFilepath();
|
||||||
|
if ($isAdmin) {
|
||||||
|
$this->view->file_name = $filePath;
|
||||||
|
} else {
|
||||||
|
$fileParts = explode(DIRECTORY_SEPARATOR, $filePath);
|
||||||
|
$filename = end($fileParts);
|
||||||
|
$this->view->file_name = $filename;
|
||||||
|
}
|
||||||
|
// 1000 B in KB and 1000 KB in MB and 1000 MB in GB
|
||||||
|
$size = $file->getPropelOrm()->getFileSize();
|
||||||
|
if ($size < 1000) {
|
||||||
|
// Use B up to 1 KB
|
||||||
|
$this->view->file_size = $size . ' B';
|
||||||
|
} elseif ($size < (500 * 1000)) {
|
||||||
|
// Use KB up to 500 KB
|
||||||
|
$this->view->file_size = round($size / 1000, 1) . ' KB';
|
||||||
|
} elseif ($size < (1 * 1000 * 1000 * 1000)) {
|
||||||
|
// Use MB up to 1 GB
|
||||||
|
$this->view->file_size = round($size / 1000 / 1000, 1) . ' MB';
|
||||||
|
} else {
|
||||||
|
$this->view->file_size = round($size / 1000 / 1000 / 1000, 1) . ' GB';
|
||||||
|
}
|
||||||
$this->view->html = $this->view->render('library/edit-file-md.phtml');
|
$this->view->html = $this->view->render('library/edit-file-md.phtml');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,20 +44,14 @@ class LoginController extends Zend_Controller_Action
|
||||||
|
|
||||||
$form = new Application_Form_Login();
|
$form = new Application_Form_Login();
|
||||||
|
|
||||||
$message = _('Please enter your username and password.');
|
$authAdapter = Application_Model_Auth::getAuthAdapter();
|
||||||
|
|
||||||
if ($request->isPost()) {
|
if ($authAdapter instanceof LibreTime_Auth_Adaptor_Header || ($request->isPost() && $form->isValid($request->getPost()))) {
|
||||||
// Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization.
|
|
||||||
// session_start();
|
|
||||||
|
|
||||||
if ($form->isValid($request->getPost())) {
|
|
||||||
// get the username and password from the form
|
// get the username and password from the form
|
||||||
$username = $form->getValue('username');
|
$username = $form->getValue('username');
|
||||||
$password = $form->getValue('password');
|
$password = $form->getValue('password');
|
||||||
$locale = $form->getValue('locale');
|
$locale = $form->getValue('locale');
|
||||||
|
|
||||||
$authAdapter = Application_Model_Auth::getAuthAdapter();
|
|
||||||
|
|
||||||
// pass to the adapter the submitted username and password
|
// pass to the adapter the submitted username and password
|
||||||
$authAdapter->setIdentity($username)
|
$authAdapter->setIdentity($username)
|
||||||
->setCredential($password);
|
->setCredential($password);
|
||||||
|
@ -83,7 +77,6 @@ class LoginController extends Zend_Controller_Action
|
||||||
$form = $this->loginError($username);
|
$form = $this->loginError($username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$this->view->form = $form;
|
$this->view->form = $form;
|
||||||
$this->view->airtimeVersion = $CC_CONFIG['airtime_version'];
|
$this->view->airtimeVersion = $CC_CONFIG['airtime_version'];
|
||||||
|
@ -108,8 +101,6 @@ class LoginController extends Zend_Controller_Action
|
||||||
|
|
||||||
public function passwordRestoreAction()
|
public function passwordRestoreAction()
|
||||||
{
|
{
|
||||||
$this->view->headScript()->appendFile(Assets::url('js/airtime/login/password-restore.js'), 'text/javascript');
|
|
||||||
|
|
||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
$stationLocale = Application_Model_Preference::GetDefaultLocale();
|
$stationLocale = Application_Model_Preference::GetDefaultLocale();
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,9 @@ class PreferenceController extends Zend_Controller_Action
|
||||||
Application_Model_Preference::SetAllow3rdPartyApi($values['thirdPartyApi']);
|
Application_Model_Preference::SetAllow3rdPartyApi($values['thirdPartyApi']);
|
||||||
Application_Model_Preference::SetDefaultLocale($values['locale']);
|
Application_Model_Preference::SetDefaultLocale($values['locale']);
|
||||||
Application_Model_Preference::SetWeekStartDay($values['weekStartDay']);
|
Application_Model_Preference::SetWeekStartDay($values['weekStartDay']);
|
||||||
|
Application_Model_Preference::setScheduleTrimOverbooked($values['scheduleTrimOverbooked']);
|
||||||
Application_Model_Preference::setRadioPageDisplayLoginButton($values['radioPageLoginButton']);
|
Application_Model_Preference::setRadioPageDisplayLoginButton($values['radioPageLoginButton']);
|
||||||
|
Application_Model_Preference::setRadioPageDisabled($values['radioPageDisabled']);
|
||||||
Application_Model_Preference::SetFeaturePreviewMode($values['featurePreviewMode']);
|
Application_Model_Preference::SetFeaturePreviewMode($values['featurePreviewMode']);
|
||||||
|
|
||||||
$logoUploadElement = $form->getSubForm('preferences_general')->getElement('stationLogo');
|
$logoUploadElement = $form->getSubForm('preferences_general')->getElement('stationLogo');
|
||||||
|
@ -156,6 +158,11 @@ class PreferenceController extends Zend_Controller_Action
|
||||||
$setting = Application_Model_StreamSetting::getStreamSetting();
|
$setting = Application_Model_StreamSetting::getStreamSetting();
|
||||||
$form->setSetting($setting);
|
$form->setSetting($setting);
|
||||||
|
|
||||||
|
if ($num_of_stream > MAX_NUM_STREAMS) {
|
||||||
|
Logging::error('Your streams count (' . $num_of_stream . ') exceed the maximum, some of them will not be displayed');
|
||||||
|
$num_of_stream = MAX_NUM_STREAMS;
|
||||||
|
}
|
||||||
|
|
||||||
for ($i = 1; $i <= $num_of_stream; ++$i) {
|
for ($i = 1; $i <= $num_of_stream; ++$i) {
|
||||||
$subform = new Application_Form_StreamSettingSubForm();
|
$subform = new Application_Form_StreamSettingSubForm();
|
||||||
$subform->setPrefix($i);
|
$subform->setPrefix($i);
|
||||||
|
@ -190,8 +197,10 @@ class PreferenceController extends Zend_Controller_Action
|
||||||
if ($changeRGenabled || $changeRGmodifier) {
|
if ($changeRGenabled || $changeRGmodifier) {
|
||||||
Application_Model_Preference::SetEnableReplayGain($values['enableReplayGain']);
|
Application_Model_Preference::SetEnableReplayGain($values['enableReplayGain']);
|
||||||
Application_Model_Preference::setReplayGainModifier($values['replayGainModifier']);
|
Application_Model_Preference::setReplayGainModifier($values['replayGainModifier']);
|
||||||
$md = ['schedule' => Application_Model_Schedule::getSchedule()];
|
// The side effects of this function are still required to fill the schedule, we
|
||||||
Application_Model_RabbitMq::SendMessageToPypo('update_schedule', $md);
|
// don't use the returned schedule.
|
||||||
|
Application_Model_Schedule::getSchedule();
|
||||||
|
Application_Model_RabbitMq::SendMessageToPypo('update_schedule', []);
|
||||||
// Application_Model_RabbitMq::PushSchedule();
|
// Application_Model_RabbitMq::PushSchedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,11 +44,9 @@ class TracktypeController extends Zend_Controller_Action
|
||||||
$formData[$v[0]] = urldecode($v[1]);
|
$formData[$v[0]] = urldecode($v[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($form->validateCode($formData)) {
|
if ($form->isValid($formData)) {
|
||||||
$tracktype = new Application_Model_Tracktype($formData['tracktype_id']);
|
$tracktype = new Application_Model_Tracktype($formData['tracktype_id']);
|
||||||
if (empty($formData['tracktype_id'])) {
|
|
||||||
$tracktype->setCode($formData['code']);
|
$tracktype->setCode($formData['code']);
|
||||||
}
|
|
||||||
$tracktype->setTypeName($formData['type_name']);
|
$tracktype->setTypeName($formData['type_name']);
|
||||||
$tracktype->setDescription($formData['description']);
|
$tracktype->setDescription($formData['description']);
|
||||||
$tracktype->setVisibility($formData['visibility']);
|
$tracktype->setVisibility($formData['visibility']);
|
||||||
|
|
|
@ -217,6 +217,13 @@ class PageLayoutInitPlugin extends Zend_Controller_Plugin_Abstract
|
||||||
->appendFile(Assets::url('js/airtime/common/common.js'), 'text/javascript')
|
->appendFile(Assets::url('js/airtime/common/common.js'), 'text/javascript')
|
||||||
->appendFile(Assets::url('js/airtime/common/audioplaytest.js'), 'text/javascript');
|
->appendFile(Assets::url('js/airtime/common/audioplaytest.js'), 'text/javascript');
|
||||||
|
|
||||||
|
// include wavesurfer.js for waveform display
|
||||||
|
$view->headScript()->appendFile(Assets::url('js/wavesurfer/wavesurfer.min.js'), 'text/javascript')
|
||||||
|
->appendFile(Assets::url('js/wavesurfer/timeline.min.js'), 'text/javascript')
|
||||||
|
->appendFile(Assets::url('js/wavesurfer/regions.min.js'), 'text/javascript')
|
||||||
|
->appendFile(Assets::url('js/wavesurfer/cursor.min.js'), 'text/javascript')
|
||||||
|
->appendFile(Assets::url('js/wavesurfer/libretime.js'), 'text/javascript');
|
||||||
|
|
||||||
$user = Application_Model_User::getCurrentUser();
|
$user = Application_Model_User::getCurrentUser();
|
||||||
if (!is_null($user)) {
|
if (!is_null($user)) {
|
||||||
$userType = $user->getType();
|
$userType = $user->getType();
|
||||||
|
|
|
@ -5,8 +5,10 @@ class RabbitMqPlugin extends Zend_Controller_Plugin_Abstract
|
||||||
public function dispatchLoopShutdown()
|
public function dispatchLoopShutdown()
|
||||||
{
|
{
|
||||||
if (Application_Model_RabbitMq::$doPush) {
|
if (Application_Model_RabbitMq::$doPush) {
|
||||||
$md = ['schedule' => Application_Model_Schedule::getSchedule()];
|
// The side effects of this function are still required to fill the schedule, we
|
||||||
Application_Model_RabbitMq::SendMessageToPypo('update_schedule', $md);
|
// don't use the returned schedule.
|
||||||
|
Application_Model_Schedule::getSchedule();
|
||||||
|
Application_Model_RabbitMq::SendMessageToPypo('update_schedule', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (memory_get_peak_usage() > 30 * 2 ** 20) {
|
if (memory_get_peak_usage() > 30 * 2 ** 20) {
|
||||||
|
|
|
@ -13,6 +13,8 @@ class Application_Form_AddShowAutoPlaylist extends Zend_Form_SubForm
|
||||||
// and store to assoc array
|
// and store to assoc array
|
||||||
$maxLens = Application_Model_Show::getMaxLengths();
|
$maxLens = Application_Model_Show::getMaxLengths();
|
||||||
|
|
||||||
|
$playlistNames = Application_Model_Library::getPlaylistNames(true);
|
||||||
|
|
||||||
// Add autoplaylist checkbox element
|
// Add autoplaylist checkbox element
|
||||||
$this->addElement('checkbox', 'add_show_has_autoplaylist', [
|
$this->addElement('checkbox', 'add_show_has_autoplaylist', [
|
||||||
'label' => _('Add Autoloading Playlist ?'),
|
'label' => _('Add Autoloading Playlist ?'),
|
||||||
|
@ -23,10 +25,11 @@ class Application_Form_AddShowAutoPlaylist extends Zend_Form_SubForm
|
||||||
|
|
||||||
$autoPlaylistSelect = new Zend_Form_Element_Select('add_show_autoplaylist_id');
|
$autoPlaylistSelect = new Zend_Form_Element_Select('add_show_autoplaylist_id');
|
||||||
$autoPlaylistSelect->setLabel(_('Select Playlist'));
|
$autoPlaylistSelect->setLabel(_('Select Playlist'));
|
||||||
$autoPlaylistSelect->setMultiOptions(Application_Model_Library::getPlaylistNames(true));
|
$autoPlaylistSelect->setMultiOptions($playlistNames);
|
||||||
$autoPlaylistSelect->setValue(null);
|
$autoPlaylistSelect->setValue(null);
|
||||||
$autoPlaylistSelect->setDecorators(['ViewHelper']);
|
$autoPlaylistSelect->setDecorators(['ViewHelper']);
|
||||||
$this->addElement($autoPlaylistSelect);
|
$this->addElement($autoPlaylistSelect);
|
||||||
|
|
||||||
// Add autoplaylist checkbox element
|
// Add autoplaylist checkbox element
|
||||||
$this->addElement('checkbox', 'add_show_autoplaylist_repeat', [
|
$this->addElement('checkbox', 'add_show_autoplaylist_repeat', [
|
||||||
'label' => _('Repeat Playlist Until Show is Full ?'),
|
'label' => _('Repeat Playlist Until Show is Full ?'),
|
||||||
|
@ -34,6 +37,23 @@ class Application_Form_AddShowAutoPlaylist extends Zend_Form_SubForm
|
||||||
'class' => 'input_text',
|
'class' => 'input_text',
|
||||||
'decorators' => ['ViewHelper'],
|
'decorators' => ['ViewHelper'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Append 'Default' to 'None' option
|
||||||
|
$playlistNames[null] = _('None') . '/' . _('Default');
|
||||||
|
|
||||||
|
$introPlaylistSelect = new Zend_Form_Element_Select('add_show_intro_playlist_id');
|
||||||
|
$introPlaylistSelect->setLabel(_('Select Intro Playlist'));
|
||||||
|
$introPlaylistSelect->setMultiOptions($playlistNames);
|
||||||
|
$introPlaylistSelect->setValue(null);
|
||||||
|
$introPlaylistSelect->setDecorators(['ViewHelper']);
|
||||||
|
$this->addElement($introPlaylistSelect);
|
||||||
|
|
||||||
|
$outroPlaylistSelect = new Zend_Form_Element_Select('add_show_outro_playlist_id');
|
||||||
|
$outroPlaylistSelect->setLabel(_('Select Outro Playlist'));
|
||||||
|
$outroPlaylistSelect->setMultiOptions($playlistNames);
|
||||||
|
$outroPlaylistSelect->setValue(null);
|
||||||
|
$outroPlaylistSelect->setDecorators(['ViewHelper']);
|
||||||
|
$this->addElement($outroPlaylistSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function disable()
|
public function disable()
|
||||||
|
|
|
@ -28,6 +28,17 @@ class Application_Form_AddTracktype extends Zend_Form
|
||||||
$code->setAttrib('style', 'width: 40%');
|
$code->setAttrib('style', 'width: 40%');
|
||||||
$code->setRequired(true);
|
$code->setRequired(true);
|
||||||
$code->addValidator($notEmptyValidator);
|
$code->addValidator($notEmptyValidator);
|
||||||
|
|
||||||
|
$uniqueTrackTypeCodeValidator = new Zend_Validate_Callback(function ($value, $context) {
|
||||||
|
if (strlen($context['tracktype_id']) === 0) { // Only check uniqueness on create
|
||||||
|
return CcTracktypesQuery::create()->filterByDbCode($value)->count() === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
$uniqueTrackTypeCodeValidator->setMessage(_('Code is not unique.'));
|
||||||
|
$code->addValidator($uniqueTrackTypeCodeValidator);
|
||||||
|
|
||||||
$code->addFilter('StringTrim');
|
$code->addFilter('StringTrim');
|
||||||
$this->addElement($code);
|
$this->addElement($code);
|
||||||
|
|
||||||
|
@ -64,19 +75,4 @@ class Application_Form_AddTracktype extends Zend_Form
|
||||||
$saveBtn->setLabel(_('Save'));
|
$saveBtn->setLabel(_('Save'));
|
||||||
$this->addElement($saveBtn);
|
$this->addElement($saveBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validateCode($data)
|
|
||||||
{
|
|
||||||
if (strlen($data['tracktype_id']) == 0) {
|
|
||||||
$count = CcTracktypesQuery::create()->filterByDbCode($data['code'])->count();
|
|
||||||
|
|
||||||
if ($count != 0) {
|
|
||||||
$this->getElement('code')->setErrors([_('Code is not unique.')]);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,11 @@ class Application_Form_EditAudioMD extends Zend_Form
|
||||||
{
|
{
|
||||||
// Set the method for the display form to POST
|
// Set the method for the display form to POST
|
||||||
$this->setMethod('post');
|
$this->setMethod('post');
|
||||||
|
$this->setAttrib('id', 'track_edit_' . $p_id);
|
||||||
|
|
||||||
$file_id = new Zend_Form_Element_Hidden('file_id');
|
$file_id = new Zend_Form_Element_Hidden('file_id');
|
||||||
$file_id->setValue($p_id);
|
$file_id->setValue($p_id);
|
||||||
|
$file_id->setDecorators(['ViewHelper']);
|
||||||
$file_id->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
$file_id->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
||||||
$file_id->removeDecorator('Label');
|
$file_id->removeDecorator('Label');
|
||||||
$file_id->setAttrib('class', 'obj_id');
|
$file_id->setAttrib('class', 'obj_id');
|
||||||
|
@ -23,25 +25,25 @@ class Application_Form_EditAudioMD extends Zend_Form
|
||||||
->setValidators([
|
->setValidators([
|
||||||
new Zend_Validate_StringLength(['max' => 2048]),
|
new Zend_Validate_StringLength(['max' => 2048]),
|
||||||
]);
|
]);
|
||||||
$file_id->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
$artwork->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
||||||
$file_id->removeDecorator('Label');
|
$artwork->removeDecorator('Label');
|
||||||
$file_id->setAttrib('class', 'artwork');
|
$artwork->setAttrib('class', 'artwork');
|
||||||
$this->addElement($artwork);
|
$this->addElement($artwork);
|
||||||
|
|
||||||
// Set artwork hidden field
|
// Set artwork hidden field
|
||||||
$set_artwork = new Zend_Form_Element_Hidden('set_artwork');
|
$set_artwork = new Zend_Form_Element_Hidden('set_artwork');
|
||||||
$set_artwork->class = 'input_text set_artwork_' . $p_id;
|
$set_artwork->class = 'input_text set_artwork_' . $p_id;
|
||||||
$file_id->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
$set_artwork->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
||||||
$file_id->removeDecorator('Label');
|
$set_artwork->removeDecorator('Label');
|
||||||
$file_id->setAttrib('class', 'set_artwork');
|
$set_artwork->setAttrib('class', 'set_artwork');
|
||||||
$this->addElement($set_artwork);
|
$this->addElement($set_artwork);
|
||||||
|
|
||||||
// Remove artwork hidden field
|
// Remove artwork hidden field
|
||||||
$remove_artwork = new Zend_Form_Element_Hidden('remove_artwork');
|
$remove_artwork = new Zend_Form_Element_Hidden('remove_artwork');
|
||||||
$remove_artwork->class = 'input_text remove_artwork_' . $p_id;
|
$remove_artwork->class = 'input_text remove_artwork_' . $p_id;
|
||||||
$file_id->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
$remove_artwork->addDecorator('HtmlTag', ['tag' => 'div', 'style' => 'display:none']);
|
||||||
$file_id->removeDecorator('Label');
|
$remove_artwork->removeDecorator('Label');
|
||||||
$file_id->setAttrib('class', 'remove_artwork');
|
$remove_artwork->setAttrib('class', 'remove_artwork');
|
||||||
$this->addElement($remove_artwork);
|
$this->addElement($remove_artwork);
|
||||||
|
|
||||||
// Add title field
|
// Add title field
|
||||||
|
@ -242,7 +244,7 @@ class Application_Form_EditAudioMD extends Zend_Form
|
||||||
$validCuePattern = '/^(?:[0-9]{1,2}:)?(?:[0-9]{1,2}:)?[0-9]{1,6}(\.\d{1,6})?$/';
|
$validCuePattern = '/^(?:[0-9]{1,2}:)?(?:[0-9]{1,2}:)?[0-9]{1,6}(\.\d{1,6})?$/';
|
||||||
|
|
||||||
$cueIn = new Zend_Form_Element_Text('cuein');
|
$cueIn = new Zend_Form_Element_Text('cuein');
|
||||||
$cueIn->class = 'input_text';
|
$cueIn->class = 'input_text cuein_' . $p_id;
|
||||||
$cueIn->setLabel('Cue In:');
|
$cueIn->setLabel('Cue In:');
|
||||||
$cueInValidator = Application_Form_Helper_ValidationTypes::overrideRegexValidator(
|
$cueInValidator = Application_Form_Helper_ValidationTypes::overrideRegexValidator(
|
||||||
$validCuePattern,
|
$validCuePattern,
|
||||||
|
@ -252,7 +254,7 @@ class Application_Form_EditAudioMD extends Zend_Form
|
||||||
$this->addElement($cueIn);
|
$this->addElement($cueIn);
|
||||||
|
|
||||||
$cueOut = new Zend_Form_Element_Text('cueout');
|
$cueOut = new Zend_Form_Element_Text('cueout');
|
||||||
$cueOut->class = 'input_text';
|
$cueOut->class = 'input_text cueout_' . $p_id;
|
||||||
$cueOut->setLabel('Cue Out:');
|
$cueOut->setLabel('Cue Out:');
|
||||||
$cueOutValidator = Application_Form_Helper_ValidationTypes::overrideRegexValidator(
|
$cueOutValidator = Application_Form_Helper_ValidationTypes::overrideRegexValidator(
|
||||||
$validCuePattern,
|
$validCuePattern,
|
||||||
|
|
|
@ -105,16 +105,18 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
|
||||||
$tracktypeDefault->setValue(Application_Model_Preference::GetTrackTypeDefault());
|
$tracktypeDefault->setValue(Application_Model_Preference::GetTrackTypeDefault());
|
||||||
$this->addElement($tracktypeDefault);
|
$this->addElement($tracktypeDefault);
|
||||||
|
|
||||||
|
$playlistNames = Application_Model_Library::getPlaylistNames(true);
|
||||||
|
|
||||||
// add intro playlist select here
|
// add intro playlist select here
|
||||||
$introPlaylistSelect = new Zend_Form_Element_Select('introPlaylistSelect');
|
$introPlaylistSelect = new Zend_Form_Element_Select('introPlaylistSelect');
|
||||||
$introPlaylistSelect->setLabel(_('Intro Autoloading Playlist'));
|
$introPlaylistSelect->setLabel(_('Intro Autoloading Playlist'));
|
||||||
$introPlaylistSelect->setMultiOptions(Application_Model_Library::getPlaylistNames(true));
|
$introPlaylistSelect->setMultiOptions($playlistNames);
|
||||||
$introPlaylistSelect->setValue(Application_Model_Preference::GetIntroPlaylist());
|
$introPlaylistSelect->setValue(Application_Model_Preference::GetIntroPlaylist());
|
||||||
$this->addElement($introPlaylistSelect);
|
$this->addElement($introPlaylistSelect);
|
||||||
|
|
||||||
$outroPlaylistSelect = new Zend_Form_Element_Select('outroPlaylistSelect');
|
$outroPlaylistSelect = new Zend_Form_Element_Select('outroPlaylistSelect');
|
||||||
$outroPlaylistSelect->setLabel(_('Outro Autoloading Playlist'));
|
$outroPlaylistSelect->setLabel(_('Outro Autoloading Playlist'));
|
||||||
$outroPlaylistSelect->setMultiOptions(Application_Model_Library::getPlaylistNames(true));
|
$outroPlaylistSelect->setMultiOptions($playlistNames);
|
||||||
$outroPlaylistSelect->setValue(Application_Model_Preference::GetOutroPlaylist());
|
$outroPlaylistSelect->setValue(Application_Model_Preference::GetOutroPlaylist());
|
||||||
$this->addElement($outroPlaylistSelect);
|
$this->addElement($outroPlaylistSelect);
|
||||||
|
|
||||||
|
@ -150,6 +152,18 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
|
||||||
]);
|
]);
|
||||||
$this->addElement($podcast_auto_smartblock);
|
$this->addElement($podcast_auto_smartblock);
|
||||||
|
|
||||||
|
$scheduleTrimOverbooked = new Zend_Form_Element_Checkbox('scheduleTrimOverbooked');
|
||||||
|
$scheduleTrimOverbooked->setDecorators([
|
||||||
|
'ViewHelper',
|
||||||
|
'Errors',
|
||||||
|
'Label',
|
||||||
|
]);
|
||||||
|
$displayScheduleTrimOverbookedValue = Application_Model_Preference::getScheduleTrimOverbooked();
|
||||||
|
$scheduleTrimOverbooked->addDecorator('Label');
|
||||||
|
$scheduleTrimOverbooked->setLabel(_('Trim overbooked shows after autoloading?'));
|
||||||
|
$scheduleTrimOverbooked->setValue($displayScheduleTrimOverbookedValue);
|
||||||
|
$this->addElement($scheduleTrimOverbooked);
|
||||||
|
|
||||||
// TODO add and insert Podcast Smartblock and Playlist autogenerate options
|
// TODO add and insert Podcast Smartblock and Playlist autogenerate options
|
||||||
|
|
||||||
$third_party_api = new Zend_Form_Element_Radio('thirdPartyApi');
|
$third_party_api = new Zend_Form_Element_Radio('thirdPartyApi');
|
||||||
|
@ -202,11 +216,24 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
|
||||||
if ($displayRadioPageLoginButtonValue == '') {
|
if ($displayRadioPageLoginButtonValue == '') {
|
||||||
$displayRadioPageLoginButtonValue = true;
|
$displayRadioPageLoginButtonValue = true;
|
||||||
}
|
}
|
||||||
$radioPageLoginButton->addDecorator('Label', ['class' => 'enable-tunein']);
|
$radioPageLoginButton->addDecorator('Label');
|
||||||
$radioPageLoginButton->setLabel(_('Display login button on your Radio Page?'));
|
$radioPageLoginButton->setLabel(_('Display login button on your Radio Page?'));
|
||||||
$radioPageLoginButton->setValue($displayRadioPageLoginButtonValue);
|
$radioPageLoginButton->setValue($displayRadioPageLoginButtonValue);
|
||||||
$this->addElement($radioPageLoginButton);
|
$this->addElement($radioPageLoginButton);
|
||||||
|
|
||||||
|
// add a checkbox for completely disabling the radio page
|
||||||
|
$radioPageDisabled = new Zend_Form_Element_Checkbox('radioPageDisabled');
|
||||||
|
$radioPageDisabled->setDecorators([
|
||||||
|
'ViewHelper',
|
||||||
|
'Errors',
|
||||||
|
'Label',
|
||||||
|
]);
|
||||||
|
$radioPageDisabledValue = Application_Model_Preference::getRadioPageDisabled();
|
||||||
|
$radioPageDisabled->addDecorator('Label');
|
||||||
|
$radioPageDisabled->setLabel(_('Disable the public radio page and redirect to the login page?'));
|
||||||
|
$radioPageDisabled->setValue($radioPageDisabledValue);
|
||||||
|
$this->addElement($radioPageDisabled);
|
||||||
|
|
||||||
$feature_preview_mode = new Zend_Form_Element_Radio('featurePreviewMode');
|
$feature_preview_mode = new Zend_Form_Element_Radio('featurePreviewMode');
|
||||||
$feature_preview_mode->setLabel(_('Feature Previews'));
|
$feature_preview_mode->setLabel(_('Feature Previews'));
|
||||||
$feature_preview_mode->setMultiOptions([
|
$feature_preview_mode->setMultiOptions([
|
||||||
|
|
|
@ -2,149 +2,11 @@
|
||||||
|
|
||||||
class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
{
|
{
|
||||||
private $criteriaOptions;
|
|
||||||
private $stringCriteriaOptions;
|
|
||||||
private $numericCriteriaOptions;
|
|
||||||
private $dateTimeCriteriaOptions;
|
|
||||||
private $timePeriodCriteriaOptions;
|
private $timePeriodCriteriaOptions;
|
||||||
private $sortOptions;
|
private $sortOptions;
|
||||||
private $limitOptions;
|
private $limitOptions;
|
||||||
private $isOrNotCriteriaOptions;
|
|
||||||
private $trackTypeOptions;
|
private $trackTypeOptions;
|
||||||
|
|
||||||
/* We need to know if the criteria value will be a string
|
|
||||||
* or numeric value in order to populate the modifier
|
|
||||||
* select list
|
|
||||||
*/
|
|
||||||
private $criteriaTypes = [
|
|
||||||
0 => '',
|
|
||||||
'album_title' => 's',
|
|
||||||
'bit_rate' => 'n',
|
|
||||||
'bpm' => 'n',
|
|
||||||
'composer' => 's',
|
|
||||||
'conductor' => 's',
|
|
||||||
'copyright' => 's',
|
|
||||||
'cuein' => 'n',
|
|
||||||
'cueout' => 'n',
|
|
||||||
'description' => 's',
|
|
||||||
'artist_name' => 's',
|
|
||||||
'encoded_by' => 's',
|
|
||||||
'utime' => 'd',
|
|
||||||
'mtime' => 'd',
|
|
||||||
'lptime' => 'd',
|
|
||||||
'genre' => 's',
|
|
||||||
'isrc_number' => 's',
|
|
||||||
'label' => 's',
|
|
||||||
'language' => 's',
|
|
||||||
'length' => 'n',
|
|
||||||
'mime' => 's',
|
|
||||||
'mood' => 's',
|
|
||||||
'owner_id' => 's',
|
|
||||||
'replay_gain' => 'n',
|
|
||||||
'sample_rate' => 'n',
|
|
||||||
'track_title' => 's',
|
|
||||||
'track_number' => 'n',
|
|
||||||
'info_url' => 's',
|
|
||||||
'year' => 'n',
|
|
||||||
'track_type_id' => 'tt',
|
|
||||||
];
|
|
||||||
|
|
||||||
private function getCriteriaOptions($option = null)
|
|
||||||
{
|
|
||||||
if (!isset($this->criteriaOptions)) {
|
|
||||||
$this->criteriaOptions = [
|
|
||||||
0 => _('Select criteria'),
|
|
||||||
'album_title' => _('Album'),
|
|
||||||
'bit_rate' => _('Bit Rate (Kbps)'),
|
|
||||||
'bpm' => _('BPM'),
|
|
||||||
'composer' => _('Composer'),
|
|
||||||
'conductor' => _('Conductor'),
|
|
||||||
'copyright' => _('Copyright'),
|
|
||||||
'cuein' => _('Cue In'),
|
|
||||||
'cueout' => _('Cue Out'),
|
|
||||||
'description' => _('Description'),
|
|
||||||
'artist_name' => _('Creator'),
|
|
||||||
'encoded_by' => _('Encoded By'),
|
|
||||||
'genre' => _('Genre'),
|
|
||||||
'isrc_number' => _('ISRC'),
|
|
||||||
'label' => _('Label'),
|
|
||||||
'language' => _('Language'),
|
|
||||||
'mtime' => _('Last Modified'),
|
|
||||||
'lptime' => _('Last Played'),
|
|
||||||
'length' => _('Length'),
|
|
||||||
'track_type_id' => _('Track Type'),
|
|
||||||
'mime' => _('Mime'),
|
|
||||||
'mood' => _('Mood'),
|
|
||||||
'owner_id' => _('Owner'),
|
|
||||||
'replay_gain' => _('Replay Gain'),
|
|
||||||
'sample_rate' => _('Sample Rate (kHz)'),
|
|
||||||
'track_title' => _('Title'),
|
|
||||||
'track_number' => _('Track Number'),
|
|
||||||
'utime' => _('Uploaded'),
|
|
||||||
'info_url' => _('Website'),
|
|
||||||
'year' => _('Year'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_null($option)) {
|
|
||||||
return $this->criteriaOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->criteriaOptions[$option];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getStringCriteriaOptions()
|
|
||||||
{
|
|
||||||
if (!isset($this->stringCriteriaOptions)) {
|
|
||||||
$this->stringCriteriaOptions = [
|
|
||||||
'0' => _('Select modifier'),
|
|
||||||
'contains' => _('contains'),
|
|
||||||
'does not contain' => _('does not contain'),
|
|
||||||
'is' => _('is'),
|
|
||||||
'is not' => _('is not'),
|
|
||||||
'starts with' => _('starts with'),
|
|
||||||
'ends with' => _('ends with'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->stringCriteriaOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getNumericCriteriaOptions()
|
|
||||||
{
|
|
||||||
if (!isset($this->numericCriteriaOptions)) {
|
|
||||||
$this->numericCriteriaOptions = [
|
|
||||||
'0' => _('Select modifier'),
|
|
||||||
'is' => _('is'),
|
|
||||||
'is not' => _('is not'),
|
|
||||||
'is greater than' => _('is greater than'),
|
|
||||||
'is less than' => _('is less than'),
|
|
||||||
'is in the range' => _('is in the range'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->numericCriteriaOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getDateTimeCriteriaOptions()
|
|
||||||
{
|
|
||||||
if (!isset($this->dateTimeCriteriaOptions)) {
|
|
||||||
$this->dateTimeCriteriaOptions = [
|
|
||||||
'0' => _('Select modifier'),
|
|
||||||
'before' => _('before'),
|
|
||||||
'after' => _('after'),
|
|
||||||
'between' => _('between'),
|
|
||||||
'is' => _('is'),
|
|
||||||
'is not' => _('is not'),
|
|
||||||
'is greater than' => _('is greater than'),
|
|
||||||
'is less than' => _('is less than'),
|
|
||||||
'is in the range' => _('is in the range'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->dateTimeCriteriaOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getTimePeriodCriteriaOptions()
|
private function getTimePeriodCriteriaOptions()
|
||||||
{
|
{
|
||||||
if (!isset($this->timePeriodCriteriaOptions)) {
|
if (!isset($this->timePeriodCriteriaOptions)) {
|
||||||
|
@ -191,19 +53,6 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
return $this->sortOptions;
|
return $this->sortOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getIsNotOptions()
|
|
||||||
{
|
|
||||||
if (!isset($this->isOrNotCriteriaOptions)) {
|
|
||||||
$this->isOrNotCriteriaOptions = [
|
|
||||||
'0' => _('Select modifier'),
|
|
||||||
'is' => _('is'),
|
|
||||||
'is not' => _('is not'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->isOrNotCriteriaOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getTracktypeOptions()
|
private function getTracktypeOptions()
|
||||||
{
|
{
|
||||||
if (!isset($this->trackTypeOptions)) {
|
if (!isset($this->trackTypeOptions)) {
|
||||||
|
@ -297,7 +146,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
}
|
}
|
||||||
// the way the everything is currently built it setups 25 smartblock criteria forms and then disables them
|
// the way the everything is currently built it setups 25 smartblock criteria forms and then disables them
|
||||||
// but this creates 29 elements
|
// but this creates 29 elements
|
||||||
$numElements = count($this->getCriteriaOptions());
|
$numElements = count(BlockCriteria::displayCriteria());
|
||||||
// loop through once for each potential criteria option ie album, composer, track
|
// loop through once for each potential criteria option ie album, composer, track
|
||||||
// criteria from different groups are separated already by the getCriteriaGrouped call
|
// criteria from different groups are separated already by the getCriteriaGrouped call
|
||||||
|
|
||||||
|
@ -335,7 +184,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
$criteria->setAttrib('class', 'input_select sp_input_select' . $invisible)
|
$criteria->setAttrib('class', 'input_select sp_input_select' . $invisible)
|
||||||
->setValue('Select criteria')
|
->setValue('Select criteria')
|
||||||
->setDecorators(['viewHelper'])
|
->setDecorators(['viewHelper'])
|
||||||
->setMultiOptions($this->getCriteriaOptions());
|
->setMultiOptions(BlockCriteria::displayCriteria());
|
||||||
// if this isn't the first criteria and there isn't an entry for it already disable it
|
// if this isn't the first criteria and there isn't an entry for it already disable it
|
||||||
if ($i != 0 && !isset($criteriaKeys[$i])) {
|
if ($i != 0 && !isset($criteriaKeys[$i])) {
|
||||||
$criteria->setAttrib('disabled', 'disabled');
|
$criteria->setAttrib('disabled', 'disabled');
|
||||||
|
@ -344,8 +193,9 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
// the j loop starts at 0 and grows for each item matching the same criteria
|
// the j loop starts at 0 and grows for each item matching the same criteria
|
||||||
// look up the criteria type using the criteriaTypes function from above based upon the criteria value
|
// look up the criteria type using the criteriaTypes function from above based upon the criteria value
|
||||||
if (isset($criteriaKeys[$i])) {
|
if (isset($criteriaKeys[$i])) {
|
||||||
$criteriaType = $this->criteriaTypes[$storedCrit['crit'][$criteriaKeys[$i]][$j]['criteria']];
|
$bCriteria = BlockCriteria::get($storedCrit['crit'][$criteriaKeys[$i]][$j]['criteria']);
|
||||||
$criteria->setValue($storedCrit['crit'][$criteriaKeys[$i]][$j]['criteria']);
|
$criteriaType = $bCriteria->type;
|
||||||
|
$criteria->setValue($bCriteria->key);
|
||||||
}
|
}
|
||||||
$this->addElement($criteria);
|
$this->addElement($criteria);
|
||||||
|
|
||||||
|
@ -361,18 +211,10 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
}
|
}
|
||||||
// determine the modifier based upon criteria type which is looked up based upon an array
|
// determine the modifier based upon criteria type which is looked up based upon an array
|
||||||
if (isset($criteriaKeys[$i])) {
|
if (isset($criteriaKeys[$i])) {
|
||||||
if ($criteriaType == 's') {
|
$criteriaModifers->setMultiOptions($bCriteria->displayModifiers());
|
||||||
$criteriaModifers->setMultiOptions($this->getStringCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'd') {
|
|
||||||
$criteriaModifers->setMultiOptions($this->getDateTimeCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'tt') {
|
|
||||||
$criteriaModifers->setMultiOptions($this->getIsNotOptions());
|
|
||||||
} else {
|
|
||||||
$criteriaModifers->setMultiOptions($this->getNumericCriteriaOptions());
|
|
||||||
}
|
|
||||||
$criteriaModifers->setValue($storedCrit['crit'][$criteriaKeys[$i]][$j]['modifier']);
|
$criteriaModifers->setValue($storedCrit['crit'][$criteriaKeys[$i]][$j]['modifier']);
|
||||||
} else {
|
} else {
|
||||||
$criteriaModifers->setMultiOptions(['0' => _('Select modifier')]);
|
$criteriaModifers->setMultiOptions(CriteriaModifier::mapToDisplay([]));
|
||||||
}
|
}
|
||||||
$this->addElement($criteriaModifers);
|
$this->addElement($criteriaModifers);
|
||||||
|
|
||||||
|
@ -381,7 +223,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
if (isset($criteriaKeys[$i])) {
|
if (isset($criteriaKeys[$i])) {
|
||||||
$modifierTest = (string) $storedCrit['crit'][$criteriaKeys[$i]][$j]['modifier'];
|
$modifierTest = (string) $storedCrit['crit'][$criteriaKeys[$i]][$j]['modifier'];
|
||||||
if (
|
if (
|
||||||
isset($criteriaType) && $criteriaType == 'tt'
|
isset($criteriaType) && $criteriaType == ModifierType::TRACK_TYPE
|
||||||
&& preg_match('/is|is not/', $modifierTest) == 1
|
&& preg_match('/is|is not/', $modifierTest) == 1
|
||||||
) {
|
) {
|
||||||
$criteriaValue = new Zend_Form_Element_Select('sp_criteria_value_' . $i . '_' . $j);
|
$criteriaValue = new Zend_Form_Element_Select('sp_criteria_value_' . $i . '_' . $j);
|
||||||
|
@ -412,14 +254,14 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
$relativeDateTime = false;
|
$relativeDateTime = false;
|
||||||
$modifierTest = (string) $storedCrit['crit'][$criteriaKeys[$i]][$j]['modifier'];
|
$modifierTest = (string) $storedCrit['crit'][$criteriaKeys[$i]][$j]['modifier'];
|
||||||
if (
|
if (
|
||||||
isset($criteriaType) && $criteriaType == 'd'
|
isset($criteriaType) && $criteriaType == ModifierType::DATE
|
||||||
&& preg_match('/before|after|between/', $modifierTest) == 1
|
&& preg_match('/before|after|between/', $modifierTest) == 1
|
||||||
) {
|
) {
|
||||||
// set relativeDatetime boolean to true so that the datetime select is displayed below
|
// set relativeDatetime boolean to true so that the datetime select is displayed below
|
||||||
$relativeDateTime = true;
|
$relativeDateTime = true;
|
||||||
$criteriaValue->setValue(filter_var($storedCrit['crit'][$criteriaKeys[$i]][$j]['value'], FILTER_SANITIZE_NUMBER_INT));
|
$criteriaValue->setValue(filter_var($storedCrit['crit'][$criteriaKeys[$i]][$j]['value'], FILTER_SANITIZE_NUMBER_INT));
|
||||||
} elseif (
|
} elseif (
|
||||||
isset($criteriaType) && $criteriaType == 'tt'
|
isset($criteriaType) && $criteriaType == ModifierType::TRACK_TYPE
|
||||||
&& preg_match('/is|is not/', $modifierTest) == 1
|
&& preg_match('/is|is not/', $modifierTest) == 1
|
||||||
) {
|
) {
|
||||||
// set relativeDatetime boolean to true so that the datetime select is displayed below
|
// set relativeDatetime boolean to true so that the datetime select is displayed below
|
||||||
|
@ -455,7 +297,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
// check if the value is stored and it is a relative datetime field
|
// check if the value is stored and it is a relative datetime field
|
||||||
if (
|
if (
|
||||||
isset($criteriaKeys[$i], $storedCrit['crit'][$criteriaKeys[$i]][$j]['value'], $criteriaType)
|
isset($criteriaKeys[$i], $storedCrit['crit'][$criteriaKeys[$i]][$j]['value'], $criteriaType)
|
||||||
&& $criteriaType == 'd'
|
&& $criteriaType == ModifierType::DATE
|
||||||
&& preg_match('/before|after|between/', $modifierTest) == 1
|
&& preg_match('/before|after|between/', $modifierTest) == 1
|
||||||
) {
|
) {
|
||||||
// need to remove any leading numbers stored in the database
|
// need to remove any leading numbers stored in the database
|
||||||
|
@ -478,7 +320,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
->setDecorators(['viewHelper']);
|
->setDecorators(['viewHelper']);
|
||||||
if (isset($criteriaKeys[$i], $storedCrit['crit'][$criteriaKeys[$i]][$j]['extra'])) {
|
if (isset($criteriaKeys[$i], $storedCrit['crit'][$criteriaKeys[$i]][$j]['extra'])) {
|
||||||
// need to check if this is a relative date time value
|
// need to check if this is a relative date time value
|
||||||
if (isset($criteriaType) && $criteriaType == 'd' && $modifierTest == 'between') {
|
if (isset($criteriaType) && $criteriaType == ModifierType::DATE && $modifierTest == 'between') {
|
||||||
// the criteria value will be a number followed by time unit and ago so set input to number part
|
// the criteria value will be a number followed by time unit and ago so set input to number part
|
||||||
$criteriaExtra->setValue(filter_var($storedCrit['crit'][$criteriaKeys[$i]][$j]['extra'], FILTER_SANITIZE_NUMBER_INT));
|
$criteriaExtra->setValue(filter_var($storedCrit['crit'][$criteriaKeys[$i]][$j]['extra'], FILTER_SANITIZE_NUMBER_INT));
|
||||||
} else {
|
} else {
|
||||||
|
@ -610,28 +452,21 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
foreach ($data['criteria'] as $critKey => $d) {
|
foreach ($data['criteria'] as $critKey => $d) {
|
||||||
$count = 1;
|
$count = 1;
|
||||||
foreach ($d as $modKey => $modInfo) {
|
foreach ($d as $modKey => $modInfo) {
|
||||||
|
$critMod = $critKey . '_' . $modKey;
|
||||||
|
$blockCriteria = BlockCriteria::get($modInfo['sp_criteria_field']);
|
||||||
if ($modKey == 0) {
|
if ($modKey == 0) {
|
||||||
$eleCrit = $this->getElement('sp_criteria_field_' . $critKey . '_' . $modKey);
|
$eleCrit = $this->getElement('sp_criteria_field_' . $critMod);
|
||||||
$eleCrit->setValue($this->getCriteriaOptions($modInfo['sp_criteria_field']));
|
$eleCrit->setValue($blockCriteria->display);
|
||||||
$eleCrit->setAttrib('disabled', null);
|
$eleCrit->setAttrib('disabled', null);
|
||||||
|
|
||||||
$eleMod = $this->getElement('sp_criteria_modifier_' . $critKey . '_' . $modKey);
|
$eleMod = $this->getElement('sp_criteria_modifier_' . $critMod);
|
||||||
$criteriaType = $this->criteriaTypes[$modInfo['sp_criteria_field']];
|
|
||||||
if ($criteriaType == 's') {
|
$eleMod->setMultiOptions($blockCriteria->displayModifiers());
|
||||||
$eleMod->setMultiOptions($this->getStringCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'n') {
|
|
||||||
$eleMod->setMultiOptions($this->getNumericCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'd') {
|
|
||||||
$eleMod->setMultiOptions($this->getDateTimeCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'tt') {
|
|
||||||
$eleMod->setMultiOptions($this->getIsNotOptions());
|
|
||||||
} else {
|
|
||||||
$eleMod->setMultiOptions(['0' => _('Select modifier')]);
|
|
||||||
}
|
|
||||||
$eleMod->setValue($modInfo['sp_criteria_modifier']);
|
$eleMod->setValue($modInfo['sp_criteria_modifier']);
|
||||||
$eleMod->setAttrib('disabled', null);
|
$eleMod->setAttrib('disabled', null);
|
||||||
|
|
||||||
$eleDatetime = $this->getElement('sp_criteria_datetime_select_' . $critKey . '_' . $modKey);
|
$eleDatetime = $this->getElement('sp_criteria_datetime_select_' . $critMod);
|
||||||
if ($this->enableDateTimeUnit($eleMod->getValue())) {
|
if ($this->enableDateTimeUnit($eleMod->getValue())) {
|
||||||
$eleDatetime->setAttrib('enabled', 'enabled');
|
$eleDatetime->setAttrib('enabled', 'enabled');
|
||||||
$eleDatetime->setValue($modInfo['sp_criteria_datetime_select']);
|
$eleDatetime->setValue($modInfo['sp_criteria_datetime_select']);
|
||||||
|
@ -639,17 +474,17 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
} else {
|
} else {
|
||||||
$eleDatetime->setAttrib('disabled', 'disabled');
|
$eleDatetime->setAttrib('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
$eleValue = $this->getElement('sp_criteria_value_' . $critKey . '_' . $modKey);
|
$eleValue = $this->getElement('sp_criteria_value_' . $critMod);
|
||||||
$eleValue->setValue($modInfo['sp_criteria_value']);
|
$eleValue->setValue($modInfo['sp_criteria_value']);
|
||||||
$eleValue->setAttrib('disabled', null);
|
$eleValue->setAttrib('disabled', null);
|
||||||
|
|
||||||
if (isset($modInfo['sp_criteria_extra'])) {
|
if (isset($modInfo['sp_criteria_extra'])) {
|
||||||
$eleExtra = $this->getElement('sp_criteria_extra_' . $critKey . '_' . $modKey);
|
$eleExtra = $this->getElement('sp_criteria_extra_' . $critMod);
|
||||||
$eleExtra->setValue($modInfo['sp_criteria_extra']);
|
$eleExtra->setValue($modInfo['sp_criteria_extra']);
|
||||||
$eleValue->setAttrib('class', 'input_text sp_extra_input_text');
|
$eleValue->setAttrib('class', 'input_text sp_extra_input_text');
|
||||||
$eleExtra->setAttrib('disabled', null);
|
$eleExtra->setAttrib('disabled', null);
|
||||||
}
|
}
|
||||||
$eleExtraDatetime = $this->getElement('sp_criteria_extra_datetime_select_' . $critKey . '_' . $modKey);
|
$eleExtraDatetime = $this->getElement('sp_criteria_extra_datetime_select_' . $critMod);
|
||||||
if ($eleMod->getValue() == 'between') {
|
if ($eleMod->getValue() == 'between') {
|
||||||
$eleExtraDatetime->setAttrib('enabled', 'enabled');
|
$eleExtraDatetime->setAttrib('enabled', 'enabled');
|
||||||
$eleExtraDatetime->setValue($modInfo['sp_criteria_extra_datetime_select']);
|
$eleExtraDatetime->setValue($modInfo['sp_criteria_extra_datetime_select']);
|
||||||
|
@ -658,45 +493,34 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
$eleExtraDatetime->setAttrib('disabled', 'disabled');
|
$eleExtraDatetime->setAttrib('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$criteria = new Zend_Form_Element_Select('sp_criteria_field_' . $critKey . '_' . $modKey);
|
$criteria = new Zend_Form_Element_Select('sp_criteria_field_' . $critMod);
|
||||||
$criteria->setAttrib('class', 'input_select sp_input_select sp-invisible')
|
$criteria->setAttrib('class', 'input_select sp_input_select sp-invisible')
|
||||||
->setValue('Select criteria')
|
->setValue('Select criteria')
|
||||||
->setDecorators(['viewHelper'])
|
->setDecorators(['viewHelper'])
|
||||||
->setMultiOptions($this->getCriteriaOptions());
|
->setMultiOptions(BlockCriteria::displayCriteria());
|
||||||
|
|
||||||
$criteriaType = $this->criteriaTypes[$modInfo['sp_criteria_field']];
|
$criteria->setValue($blockCriteria->display);
|
||||||
$criteria->setValue($this->getCriteriaOptions($modInfo['sp_criteria_field']));
|
|
||||||
$this->addElement($criteria);
|
$this->addElement($criteria);
|
||||||
|
|
||||||
// MODIFIER
|
// MODIFIER
|
||||||
$criteriaModifers = new Zend_Form_Element_Select('sp_criteria_modifier_' . $critKey . '_' . $modKey);
|
$criteriaModifers = new Zend_Form_Element_Select('sp_criteria_modifier_' . $critMod);
|
||||||
$criteriaModifers->setValue('Select modifier')
|
$criteriaModifers->setValue('Select modifier')
|
||||||
->setAttrib('class', 'input_select sp_input_select')
|
->setAttrib('class', 'input_select sp_input_select')
|
||||||
->setDecorators(['viewHelper']);
|
->setDecorators(['viewHelper']);
|
||||||
|
|
||||||
if ($criteriaType == 's') {
|
$criteriaModifers->setMultiOptions($blockCriteria->displayModifiers());
|
||||||
$criteriaModifers->setMultiOptions($this->getStringCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'n') {
|
|
||||||
$criteriaModifers->setMultiOptions($this->getNumericCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'd') {
|
|
||||||
$criteriaModifers->setMultiOptions($this->getDateTimeCriteriaOptions());
|
|
||||||
} elseif ($criteriaType == 'tt') {
|
|
||||||
$criteriaModifers->setMultiOptions($this->getIsNotOptions());
|
|
||||||
} else {
|
|
||||||
$criteriaModifers->setMultiOptions(['0' => _('Select modifier')]);
|
|
||||||
}
|
|
||||||
$criteriaModifers->setValue($modInfo['sp_criteria_modifier']);
|
$criteriaModifers->setValue($modInfo['sp_criteria_modifier']);
|
||||||
$this->addElement($criteriaModifers);
|
$this->addElement($criteriaModifers);
|
||||||
|
|
||||||
// VALUE
|
// VALUE
|
||||||
$criteriaValue = new Zend_Form_Element_Text('sp_criteria_value_' . $critKey . '_' . $modKey);
|
$criteriaValue = new Zend_Form_Element_Text('sp_criteria_value_' . $critMod);
|
||||||
$criteriaValue->setAttrib('class', 'input_text sp_input_text')
|
$criteriaValue->setAttrib('class', 'input_text sp_input_text')
|
||||||
->setDecorators(['viewHelper']);
|
->setDecorators(['viewHelper']);
|
||||||
$criteriaValue->setValue($modInfo['sp_criteria_value']);
|
$criteriaValue->setValue($modInfo['sp_criteria_value']);
|
||||||
$this->addElement($criteriaValue);
|
$this->addElement($criteriaValue);
|
||||||
// DATETIME UNIT SELECT
|
// DATETIME UNIT SELECT
|
||||||
|
|
||||||
$criteriaDatetimeSelect = new Zend_Form_Element_Select('sp_criteria_datetime_select_' . $critKey . '_' . $modKey);
|
$criteriaDatetimeSelect = new Zend_Form_Element_Select('sp_criteria_datetime_select_' . $critMod);
|
||||||
$criteriaDatetimeSelect->setAttrib('class', 'input_select sp_input_select')
|
$criteriaDatetimeSelect->setAttrib('class', 'input_select sp_input_select')
|
||||||
->setDecorators(['viewHelper']);
|
->setDecorators(['viewHelper']);
|
||||||
if ($this->enableDateTimeUnit($criteriaValue->getValue())) {
|
if ($this->enableDateTimeUnit($criteriaValue->getValue())) {
|
||||||
|
@ -708,7 +532,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
$criteriaDatetimeSelect->setAttrib('disabled', 'disabled');
|
$criteriaDatetimeSelect->setAttrib('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
// EXTRA
|
// EXTRA
|
||||||
$criteriaExtra = new Zend_Form_Element_Text('sp_criteria_extra_' . $critKey . '_' . $modKey);
|
$criteriaExtra = new Zend_Form_Element_Text('sp_criteria_extra_' . $critMod);
|
||||||
$criteriaExtra->setAttrib('class', 'input_text sp_extra_input_text')
|
$criteriaExtra->setAttrib('class', 'input_text sp_extra_input_text')
|
||||||
->setDecorators(['viewHelper']);
|
->setDecorators(['viewHelper']);
|
||||||
if (isset($modInfo['sp_criteria_extra'])) {
|
if (isset($modInfo['sp_criteria_extra'])) {
|
||||||
|
@ -721,7 +545,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
|
|
||||||
// EXTRA DATETIME UNIT SELECT
|
// EXTRA DATETIME UNIT SELECT
|
||||||
|
|
||||||
$criteriaExtraDatetimeSelect = new Zend_Form_Element_Select('sp_criteria_extra_datetime_select_' . $critKey . '_' . $modKey);
|
$criteriaExtraDatetimeSelect = new Zend_Form_Element_Select('sp_criteria_extra_datetime_select_' . $critMod);
|
||||||
$criteriaExtraDatetimeSelect->setAttrib('class', 'input_select sp_input_select')
|
$criteriaExtraDatetimeSelect->setAttrib('class', 'input_select sp_input_select')
|
||||||
->setDecorators(['viewHelper']);
|
->setDecorators(['viewHelper']);
|
||||||
if ($criteriaValue->getValue() == 'between') {
|
if ($criteriaValue->getValue() == 'between') {
|
||||||
|
@ -761,45 +585,13 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
{
|
{
|
||||||
$isValid = true;
|
$isValid = true;
|
||||||
$data = $this->preValidation($params);
|
$data = $this->preValidation($params);
|
||||||
$criteria2PeerMap = [
|
$allCriteria = BlockCriteria::criteriaMap();
|
||||||
0 => 'Select criteria',
|
|
||||||
'album_title' => 'DbAlbumTitle',
|
|
||||||
'artist_name' => 'DbArtistName',
|
|
||||||
'bit_rate' => 'DbBitRate',
|
|
||||||
'bpm' => 'DbBpm',
|
|
||||||
'composer' => 'DbComposer',
|
|
||||||
'conductor' => 'DbConductor',
|
|
||||||
'copyright' => 'DbCopyright',
|
|
||||||
'cuein' => 'DbCuein',
|
|
||||||
'cueout' => 'DbCueout',
|
|
||||||
'description' => 'DbDescription',
|
|
||||||
'encoded_by' => 'DbEncodedBy',
|
|
||||||
'utime' => 'DbUtime',
|
|
||||||
'mtime' => 'DbMtime',
|
|
||||||
'lptime' => 'DbLPtime',
|
|
||||||
'genre' => 'DbGenre',
|
|
||||||
'info_url' => 'DbInfoUrl',
|
|
||||||
'isrc_number' => 'DbIsrcNumber',
|
|
||||||
'label' => 'DbLabel',
|
|
||||||
'language' => 'DbLanguage',
|
|
||||||
'length' => 'DbLength',
|
|
||||||
'mime' => 'DbMime',
|
|
||||||
'mood' => 'DbMood',
|
|
||||||
'owner_id' => 'DbOwnerId',
|
|
||||||
'replay_gain' => 'DbReplayGain',
|
|
||||||
'sample_rate' => 'DbSampleRate',
|
|
||||||
'track_title' => 'DbTrackTitle',
|
|
||||||
'track_number' => 'DbTrackNumber',
|
|
||||||
'year' => 'DbYear',
|
|
||||||
'track_type_id' => 'DbTrackTypeId',
|
|
||||||
];
|
|
||||||
|
|
||||||
// things we need to check
|
// things we need to check
|
||||||
// 1. limit value shouldn't be empty and has upperbound of 24 hrs
|
// 1. limit value shouldn't be empty and has upperbound of 24 hrs
|
||||||
// 2. sp_criteria or sp_criteria_modifier shouldn't be 0
|
// 2. sp_criteria or sp_criteria_modifier shouldn't be 0
|
||||||
// 3. validate formate according to DB column type
|
// 3. validate formate according to DB column type
|
||||||
$multiplier = 1;
|
$multiplier = 1;
|
||||||
$result = 0;
|
|
||||||
|
|
||||||
// validation start
|
// validation start
|
||||||
if ($data['etc']['sp_limit_options'] == 'hours') {
|
if ($data['etc']['sp_limit_options'] == 'hours') {
|
||||||
|
@ -840,7 +632,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
|
||||||
$element->addError(_('You must select Criteria and Modifier'));
|
$element->addError(_('You must select Criteria and Modifier'));
|
||||||
$isValid = false;
|
$isValid = false;
|
||||||
} else {
|
} else {
|
||||||
$column = CcFilesPeer::getTableMap()->getColumnByPhpName($criteria2PeerMap[$d['sp_criteria_field']]);
|
$column = CcFilesPeer::getTableMap()->getColumnByPhpName($allCriteria[$d['sp_criteria_field']]->peer);
|
||||||
// validation on type of column
|
// validation on type of column
|
||||||
if (in_array($d['sp_criteria_field'], ['length', 'cuein', 'cueout'])) {
|
if (in_array($d['sp_criteria_field'], ['length', 'cuein', 'cueout'])) {
|
||||||
if (!preg_match('/^(\d{2}):(\d{2}):(\d{2})/', $d['sp_criteria_value'])) {
|
if (!preg_match('/^(\d{2}):(\d{2}):(\d{2})/', $d['sp_criteria_value'])) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Airtime_Zend_Log extends Zend_Log
|
||||||
*/
|
*/
|
||||||
protected $_origErrorHandler;
|
protected $_origErrorHandler;
|
||||||
|
|
||||||
public function __construct(Zend_Log_Writer_Abstract $writer = null)
|
public function __construct(?Zend_Log_Writer_Abstract $writer = null)
|
||||||
{
|
{
|
||||||
parent::__construct($writer);
|
parent::__construct($writer);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue