61 Commits

Author SHA1 Message Date
arian ded42ccd03 chore(deps): bump python dependencies
Build and Test Debian Package / build (push) Successful in 33s
Build and Test Debian Package / test (push) Successful in 52s
2026-06-04 19:36:30 -04:00
arian 9255f9b052 refactor(tests): remove a test
Build and Test Debian Package / build (push) Successful in 28s
Build and Test Debian Package / test (push) Successful in 30s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-06-01 03:37:18 -04:00
arian 6e22dcdda5 fix(tests): change edge case file extension
Build and Test Debian Package / build (push) Successful in 27s
Build and Test Debian Package / test (push) Failing after 32s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-06-01 03:34:41 -04:00
arian ef1e42bcbb feat(tests): additional tests for uploads
Build and Test Debian Package / build (push) Successful in 28s
Build and Test Debian Package / test (push) Failing after 31s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-06-01 03:31:26 -04:00
arian 0912c338e3 chore(debian): v0.3.0 changelog added
Build and Test Debian Package / build (push) Successful in 7m11s
Build and Test Debian Package / test (push) Successful in 3m50s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 09:03:35 -04:00
arian 0aff62d050 Merge branch 'e2e-testing'
Build and Test Debian Package / build (push) Successful in 7s
Build and Test Debian Package / test (push) Successful in 4s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 08:21:47 -04:00
arian 15bcd258af feat(ci): update workflow to include testing after deb package build
Build and Test Debian Package / build (push) Successful in 7s
Build and Test Debian Package / test (push) Successful in 55s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 08:16:09 -04:00
arian 7599093d90 feat(e2e): add Dockerfile and entrypoint script for testing deb packages
Build Debian Package / build (push) Successful in 7s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 08:06:48 -04:00
arian b495a67bb8 e2e(api): add initial functional tests & pytest configuration
Build Debian Package / build (push) Successful in 8s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 08:04:25 -04:00
arian df8b20edfb Merge branch 'pr-arch'
Build Debian Package / build (push) Successful in 7s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 02:23:16 -04:00
arian 671e38429c fix(debian): change architecture from 'all' to 'amd64'
Build Debian Package / build (push) Successful in 7s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 02:19:49 -04:00
arian ce8165271d ci(build): dynamically retrieve Debian package filename
Build Debian Package / build (push) Successful in 6s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-31 02:17:40 -04:00
arian 56570e7b00 Merge branch 'dropzone-upstream'
Build Debian Package / build (push) Successful in 6s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-28 21:54:25 -04:00
arian 9922467cb6 chore(deps): update Dropzone.js upstream source
Build Debian Package / build (push) Successful in 8s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-28 21:51:18 -04:00
arian 8e105d8b93 Merge branch 'actions'
Build Debian Package / build (push) Successful in 6s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-28 19:32:00 -04:00
arian 5acd4ac728 ci(build): add branch and tag triggers for build workflow
Build Debian Package / build (push) Successful in 7s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-28 19:29:41 -04:00
arian 387c139274 Merge branch 'pr-templates-rm'
Build Debian Package / build (push) Successful in 8s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 18:13:10 -04:00
arian 95556d4d84 refactor(upload): remove unused templates
Build Debian Package / build (push) Successful in 9s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 18:05:06 -04:00
arian a56203f4f5 Merge branch 'actions'
Build Debian Package / build (push) Successful in 6s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 17:44:47 -04:00
arian 67e9ba55bf ci(debian): downgrade upload-artifact action to v3
Build Debian Package / build (push) Successful in 17s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 00:47:09 -04:00
arian 62d70d4726 ci(docker): update build script to copy output from container
Build Debian Package / build (push) Failing after 8s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 00:45:12 -04:00
arian fc162bc6ee Revert "ci(debian): fix permissions issue with deb building workflow"
Build Debian Package / build (push) Successful in 8s
This reverts commit 053be5dbe5.

Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 00:41:31 -04:00
arian 053be5dbe5 ci(debian): fix permissions issue with deb building workflow
Build Debian Package / build (push) Successful in 8s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 00:34:46 -04:00
arian 310ebfe903 ci(debian): add gitea workflow for automated deb package building
Build Debian Package / build (push) Successful in 1m28s
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-27 00:21:46 -04:00
arian f3743cc9c0 Merge branch 'deb-packaging' 2026-05-25 18:48:04 -04:00
arian 9cc751e694 fix(debian): fix systemd path issue 2026-05-25 18:43:03 -04:00
arian df6a13f978 chore(debian): v0.2.0 changelog added 2026-05-25 18:32:58 -04:00
arian 58a8200968 fix(debian): added dependencies to deb-building Dockerfile for offline installation 2026-05-25 18:15:58 -04:00
arian c7e04d931f fix(debian): fix build dependencies for offline installation 2026-05-25 18:11:13 -04:00
arian 772c8e56ec build(debian): offline pip dependency installation 2026-05-25 18:00:49 -04:00
arian d8af833ce0 chore(deps): bump python dependencies 2026-05-25 17:40:12 -04:00
arian b57dc7d81c docs(license): add MIT license 2026-05-25 17:37:44 -04:00
arian 5fa95f0814 chore(deps): bump python dependencies
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-20 15:44:49 -04:00
arian a64da5ef51 build(docker): implement docker build automation script
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-09 14:46:58 -04:00
arian ec2dfb3491 Merge branch 'docker-build' 2026-05-09 14:38:38 -04:00
arian 3dec409169 build(docker): implement containerized debian packaging
Signed-off-by: Arian Nasr <arian@2ari.ca>
2026-05-09 14:35:51 -04:00
arian 4130cc4269 chore(deps): bump gunicorn==26.0.0 2026-05-09 13:42:51 -04:00
arian 0893d21ded perf(upload) add MAX_CONTENT_LENGTH to prevent DoS 2026-05-01 06:30:12 -04:00
arian 4278fd530d chore: bump pip==26.1 2026-04-26 23:16:27 -04:00
arian 483b0fd7b0 bump packaging==26.2 2026-04-26 23:13:11 -04:00
arian c7f2c99c6b chore: bump click==8.3.3 2026-04-22 19:58:48 -04:00
arian 1d3ae30cc9 pin addl. pypi packages 2026-04-21 14:37:35 -04:00
arian 8b30c88a6a Merge branch 'pr-apt-purge-fix' 2026-04-16 11:58:56 -04:00
arian 0fc4717a05 fix: apt purge leaving pycache & warning systemd folder not empty 2026-04-16 11:57:25 -04:00
arian 2e68ad7323 v0.1.0-2 changelog 2026-04-14 12:14:13 -04:00
arian 792465dab2 run deb package pip install stage as navidrome-uploader user instead of root 2026-04-14 12:09:54 -04:00
arian 7f06ffe6d7 disable gunicorn control socket in service & patch notes 2026-04-09 02:01:06 -04:00
arian 9dc6aa8d04 tidying 2026-04-08 23:29:05 -04:00
arian 163a642f5a Merge branch 'deb-packaging' 2026-04-08 23:21:57 -04:00
arian 2cc5945e1d Merge branch 'deb-packaging-test' into deb-packaging 2026-04-08 23:20:16 -04:00
arian 8adbb87fc3 add navidrome music dir to service readwritepaths 2026-04-08 23:06:38 -04:00
arian 399544dc50 remove unused dependency 2026-04-08 05:51:54 -04:00
arian 822c3941fd remove README.md 2026-04-07 03:22:25 -04:00
arian 210fb30059 test framework for deb building 2026-04-07 03:20:40 -04:00
arian ebe75427af change default .env bind address 2026-04-07 02:49:01 -04:00
arian d0fe37033c deb build framework 2026-04-07 02:43:39 -04:00
arian 8cdacba0d6 Merge remote-tracking branch 'origin/deb-packaging' into deb-packaging 2026-04-05 17:26:48 -04:00
arian 597a02a5ed add preinstall script for deb package 2026-04-05 17:24:30 -04:00
arian 290730f413 Merge branch 'pr-systemd-service' 2026-04-05 17:22:40 -04:00
arian a4e896158d add preinstall script for deb package 2026-04-05 17:19:31 -04:00
arian e68d675f4b Merge branch 'pr-systemd-service' 2026-04-05 17:07:24 -04:00
33 changed files with 466 additions and 50 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
NAVIDROME_MUSIC_FOLDER="/opt/navidrome/music"
BIND_ADDRESS="192.168.2.24"
BIND_ADDRESS="0.0.0.0"
BIND_PORT="5001"
+62
View File
@@ -0,0 +1,62 @@
name: Build and Test Debian Package
on:
push:
branches:
- '**'
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
outputs:
deb_filename: ${{ steps.get_deb_name.outputs.filename }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Make scripts executable
run: |
chmod +x release/build-docker.sh
chmod +x release/build-deb.sh
- name: Build Debian Package
run: ./release/build-docker.sh
- name: Get Deb Filename
id: get_deb_name
run: |
DEB_FILENAME=$(ls output/*.deb | xargs basename)
echo "filename=$DEB_FILENAME" >> $GITHUB_OUTPUT
- name: Upload Build Artifact
uses: actions/upload-artifact@v3
with:
name: ${{ steps.get_deb_name.outputs.filename }}
path: output/${{ steps.get_deb_name.outputs.filename }}
retention-days: 5
test:
runs-on: ubuntu-latest
needs: build # only run tests if the build job succeeded
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create output directory
run: mkdir -p output
- name: Download Build Artifact
uses: actions/download-artifact@v3
with:
name: ${{ needs.build.outputs.deb_filename }}
path: output/
- name: Build Test Docker Image
run: docker build -t uploader-tester -f Dockerfile.test .
- name: Run E2E Test Suite
run: docker run --rm uploader-tester
+3
View File
@@ -3,3 +3,6 @@ setup.sh
navidrome-upload.service
.idea/
.env
/README.md
__pycache__/
*.deb
+29
View File
@@ -0,0 +1,29 @@
# Navidome-Uploader Dockerfile for building .deb packages
# Arian Nasr
# May 9, 2026
FROM debian:13-slim
# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
build-essential \
debhelper \
devscripts \
fakeroot \
python3 \
python3-venv \
python3-pip \
python3-wheel \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build/src
RUN mkdir -p /dist
COPY . .
RUN chmod +x release/build-deb.sh
CMD ["sh", "-c", "./release/build-deb.sh && mv ../*.deb /dist/"]
+36
View File
@@ -0,0 +1,36 @@
# Navidome-Uploader Dockerfile for testing .deb packages
# Arian Nasr
# May 31, 2026
FROM debian:stable
# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
curl \
python3 \
python3-venv \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Copy the built deb package into the container
COPY output/*.deb /tmp/navidrome-uploader.deb
RUN apt-get update && \
apt-get install -y -f /tmp/navidrome-uploader.deb && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /opt/navidrome/music && \
chown -R navidrome-uploader:navidrome-uploader /opt/navidrome/music
COPY e2e/ /e2e/
RUN python3 -m venv /e2e/venv && \
/e2e/venv/bin/pip install --no-cache-dir -r /e2e/requirements.txt
RUN chmod +x /e2e/test-entrypoint.sh
WORKDIR /e2e
ENTRYPOINT ["/e2e/test-entrypoint.sh"]
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2026 Arian Nasr (arian-nasr) - arian@2ari.ca
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+2 -2
View File
@@ -9,7 +9,7 @@ Group=navidrome-uploader
WorkingDirectory=/opt/navidrome-uploader
Environment="PATH=/opt/navidrome-uploader/venv/bin"
EnvironmentFile=/etc/default/navidrome-uploader/.env
ExecStart=/opt/navidrome-uploader/venv/bin/gunicorn -c gunicorn.conf.py main:app
ExecStart=/opt/navidrome-uploader/venv/bin/gunicorn --no-control-socket -c gunicorn.conf.py main:app
Restart=on-failure
RestartSec=30
@@ -20,7 +20,7 @@ AmbientCapabilities=
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/opt/navidrome-uploader
ReadWritePaths=/opt/navidrome-uploader /opt/navidrome/music
InaccessiblePaths=/boot /mnt /media
PrivateDevices=yes
+54
View File
@@ -0,0 +1,54 @@
navidrome-uploader (0.3.0) unstable; urgency=medium
* E2E Testing:
- Add Dockerfile and entrypoint script for testing .deb packages
- Implement initial API functional tests with pytest configuration
- Update CI workflow to run E2E tests automatically after package build
* Packaging:
- Change architecture from 'all' to 'amd64'
* CI/Build:
- Implement automated Gitea workflow for .deb building
* Refactoring:
- Remove unused templates
* Maintenance:
- Update Dropzone.js upstream source
-- Arian Nasr <arian@2ari.ca> Sun, 31 May 2026 08:43:00 -0400
navidrome-uploader (0.2.0) unstable; urgency=medium
* Packaging:
- Bundle pip wheels during build for offline host installation
- Ensure debian build environment includes pip and wheel modules
* Docker:
- Implement containerized debian packaging automation scripts
* Security:
- Add MAX_CONTENT_LENGTH to prevent upload DoS vectors
* Bug Fixes:
- Fix apt purge leaving pycache and systemd directory remnants
* Frontend:
- Change Dropzone.js upstream source location
* Maintenance:
- Add the open source software MIT license
- Upstream package & dependency updates
-- Arian Nasr <arian@2ari.ca> Mon, 25 May 2026 18:31:00 -0400
navidrome-uploader (0.1.0-2) unstable; urgency=high
* Run pip install stage as navidrome-uploader user instead of root
-- Arian Nasr <arian@2ari.ca> Tue, 14 Apr 2026 12:11:00 -0400
navidrome-uploader (0.1.0-1) unstable; urgency=medium
* Disable gunicorn control socket in systemd service unit
-- Arian Nasr <arian@2ari.ca> Thu, 09 Apr 2026 01:58:00 -0400
navidrome-uploader (0.1.0) unstable; urgency=medium
* Add Debian packaging with systemd service integration and venv setup.
-- Arian Nasr <arian@2ari.ca> Tue, 07 Apr 2026 12:00:00 +0000
+12
View File
@@ -0,0 +1,12 @@
Source: navidrome-uploader
Section: web
Priority: optional
Maintainer: Arian Nasr <arian@2ari.ca>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.7.0
Rules-Requires-Root: no
Package: navidrome-uploader
Architecture: amd64
Depends: ${misc:Depends}, adduser, python3, python3-venv, python3-pip, python3-wheel
Description: Navidrome Web Upload Utility
+7
View File
@@ -0,0 +1,7 @@
opt/navidrome-uploader
opt/navidrome-uploader/templates
opt/navidrome-uploader/static
opt/navidrome-uploader/static/css
opt/navidrome-uploader/static/js
etc/default/navidrome-uploader
+9
View File
@@ -0,0 +1,9 @@
main.py opt/navidrome-uploader/
gunicorn.conf.py opt/navidrome-uploader/
requirements.txt opt/navidrome-uploader/
.env.example opt/navidrome-uploader/
templates/* opt/navidrome-uploader/templates/
static/css/* opt/navidrome-uploader/static/css/
static/js/* opt/navidrome-uploader/static/js/
contrib/navidrome-uploader.service lib/systemd/system/
debian/wheels/* opt/navidrome-uploader/wheels/
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
set -e
APP_DIR="/opt/navidrome-uploader"
VENV_DIR="${APP_DIR}/venv"
APP_USER="navidrome-uploader"
case "$1" in
configure)
chown -R "$APP_USER:$APP_USER" "$APP_DIR"
runuser -u "$APP_USER" -- python3 -m venv "$VENV_DIR"
runuser -u "$APP_USER" -- "$VENV_DIR/bin/pip" install --no-cache-dir --no-index --find-links="$APP_DIR/wheels" -r "$APP_DIR/requirements.txt"
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload || true
systemctl enable navidrome-uploader.service || true
systemctl restart navidrome-uploader.service || true
fi
;;
esac
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -e
if command -v systemctl > /dev/null 2>&1; then
systemctl daemon-reload || true
fi
if [ "$1" = "purge" ]; then
rm -rf /etc/default/navidrome-uploader
rm -rf /opt/navidrome-uploader/venv
rm -rf /opt/navidrome-uploader/__pycache__
fi
exit 0
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
set -e
if ! getent passwd navidrome-uploader > /dev/null 2>&1; then
printf "Creating navidrome-uploader user\n"
useradd --system --shell /usr/sbin/nologin --user-group navidrome-uploader
fi
exit 0
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
remove|deconfigure)
if command -v systemctl > /dev/null 2>&1; then
systemctl stop navidrome-uploader.service || true
systemctl disable navidrome-uploader.service || true
fi
;;
esac
exit 0
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_build:
dh_auto_build
mkdir -p debian/wheels
python3 -m pip wheel --no-cache-dir -r requirements.txt pip -w debian/wheels
override_dh_install:
dh_install
install -d debian/navidrome-uploader/etc/default/navidrome-uploader
install -m 0640 .env.example debian/navidrome-uploader/etc/default/navidrome-uploader/.env
+2
View File
@@ -0,0 +1,2 @@
3.0 (native)
+3
View File
@@ -0,0 +1,3 @@
Werkzeug==3.1.8
pytest==9.0.3
requests==2.34.2
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
export NAVIDROME_MUSIC_FOLDER="/opt/navidrome/music"
export BASE_URL="http://127.0.0.1:5001"
# Start the app manually since systemd isn't running in the container
cd /opt/navidrome-uploader
su -s /bin/sh navidrome-uploader -c "/opt/navidrome-uploader/venv/bin/gunicorn --no-control-socket -c gunicorn.conf.py main:app" &
APP_PID=$!
TIMEOUT=30
SERVICE_UP=0
while [ $TIMEOUT -gt 0 ]; do
if curl -s $BASE_URL/ping | grep -q "pong"; then
SERVICE_UP=1
break
fi
sleep 1
TIMEOUT=$((TIMEOUT-1))
done
if [ $SERVICE_UP -eq 0 ]; then
echo "Error: Service failed to start within the timeout period"
kill $APP_PID
exit 1
fi
cd /e2e
/e2e/venv/bin/pytest unit/api/test_api.py
# Capture the exit code of pytest
TEST_EXIT_CODE=$?
kill $APP_PID
exit $TEST_EXIT_CODE
+55
View File
@@ -0,0 +1,55 @@
import pytest
import requests
import io
import os
from werkzeug.utils import secure_filename
def test_api_ping(base_url):
response = requests.get(f'{base_url}/ping')
assert response.status_code == 200
assert response.text == 'pong'
def test_api_upload_non_audio_file(base_url, upload_folder):
files = {'file': ('test.txt', io.BytesIO(b'not an audio file'))}
expected_filename = os.path.join(upload_folder, secure_filename('test.txt'))
response = requests.post(f'{base_url}/', files=files)
assert response.status_code == 400
assert "not allowed" in response.json().get("error", "")
assert not os.path.exists(expected_filename)
if os.path.exists(expected_filename):
os.remove(expected_filename)
def test_api_upload_mp3_file(base_url, upload_folder):
files = {'file': ('test.mp3', io.BytesIO(b'fake mp3 content'))}
expected_filename = os.path.join(upload_folder, secure_filename('test.mp3'))
response = requests.post(f'{base_url}/', files=files)
assert response.status_code == 200
assert "uploaded successfully" in response.json().get("message", "")
assert os.path.exists(expected_filename)
if os.path.exists(expected_filename):
os.remove(expected_filename)
def test_api_upload_flac_file(base_url, upload_folder):
files = {'file': ('test.flac', io.BytesIO(b'fake flac content'))}
expected_filename = os.path.join(upload_folder, secure_filename('test.flac'))
response = requests.post(f'{base_url}/', files=files)
assert response.status_code == 200
assert "uploaded successfully" in response.json().get("message", "")
assert os.path.exists(expected_filename)
if os.path.exists(expected_filename):
os.remove(expected_filename)
def test_api_upload_m4a_file(base_url, upload_folder):
files = {'file': ('test.m4a', io.BytesIO(b'fake m4a content'))}
expected_filename = os.path.join(upload_folder, secure_filename('test.m4a'))
response = requests.post(f'{base_url}/', files=files)
assert response.status_code == 400
assert "not allowed" in response.json().get("error", "")
assert not os.path.exists(expected_filename)
if os.path.exists(expected_filename):
os.remove(expected_filename)
+10
View File
@@ -0,0 +1,10 @@
import pytest
import os
@pytest.fixture(scope='session')
def base_url():
return os.getenv('BASE_URL', 'http://127.0.0.1:5001')
@pytest.fixture(scope='session')
def upload_folder():
return os.getenv('NAVIDROME_MUSIC_FOLDER', '/opt/navidrome/music')
+7 -3
View File
@@ -3,14 +3,16 @@
# March 6, 2026
import os
from flask import Flask, request, render_template
from flask import Flask, request, render_template, jsonify
from werkzeug.utils import secure_filename
UPLOAD_FOLDER = os.environ.get('NAVIDROME_MUSIC_FOLDER', '/opt/navidrome/music')
ALLOWED_EXTENSIONS = {'flac', 'mp3', 'wav'}
MAX_CONTENT_LENGTH = 500 * 1024 * 1024
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
def allowed_file(filename):
return '.' in filename and \
@@ -23,13 +25,15 @@ def ping():
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
uploaded_count = 0
for key, file in request.files.items():
if key.startswith('file') and file and allowed_file(file.filename) and file.filename != '':
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
uploaded_count += 1
else:
return render_template('error.html', error_message=f'File is not allowed.'), 400
return jsonify({"error": f"File '{file.filename}' is not allowed or is empty."}), 400
return render_template('success.html', success_message=f'{len(request.files)} file(s) uploaded successfully!'), 200
return jsonify({"message": f"{uploaded_count} file(s) uploaded successfully!"}), 200
return render_template('index.html'), 200
View File
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
PROJECT_ROOT="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_ROOT}"
dpkg-buildpackage -us -uc -b
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
PROJECT_ROOT="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_ROOT}"
docker build --platform linux/amd64 -t uploader-builder -f Dockerfile.build .
docker run --name uploader-builder-container --platform linux/amd64 uploader-builder
docker cp uploader-builder-container:/dist/. output/
docker rm uploader-builder-container
+4 -2
View File
@@ -1,8 +1,10 @@
blinker==1.9.0
click==8.3.2
click==8.4.1
Flask==3.1.3
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
Werkzeug==3.1.8
gunicorn==25.3.0
gunicorn==26.0.0
pip==26.1.2
packaging==26.2
+1
View File
@@ -0,0 +1 @@
@keyframes passing-through{0%{opacity:0;transform:translateY(40px)}30%,70%{opacity:1;transform:translateY(0px)}100%{opacity:0;transform:translateY(-40px)}}@keyframes slide-in{0%{opacity:0;transform:translateY(40px)}30%{opacity:1;transform:translateY(0px)}}@keyframes pulse{0%{transform:scale(1)}10%{transform:scale(1.1)}20%{transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:1px solid rgba(0,0,0,.8);border-radius:5px;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:.5}.dropzone .dz-message{text-align:center;margin:3em 0}.dropzone .dz-message .dz-button{background:none;color:inherit;border:none;padding:0;font:inherit;cursor:pointer;outline:inherit}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:#fff}.dropzone .dz-preview.dz-image-preview .dz-details{transition:opacity .2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,.8);background-color:hsla(0,0%,100%,.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid rgba(0,0,0,0)}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:hsla(0,0%,100%,.4);padding:0 .4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{transform:scale(1.05, 1.05);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px;background:rgba(0,0,0,.8);border-radius:50%}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px;fill:#fff}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;transition:all .2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;transition:opacity .4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:20px;top:50%;margin-top:-10px;left:15%;right:15%;border:3px solid rgba(0,0,0,.8);background:rgba(0,0,0,.8);border-radius:10px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#fff;display:block;position:relative;height:100%;width:0;transition:width 300ms ease-in-out;border-radius:17px}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;transition:opacity .3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#b10606;padding:.5em 1em;color:#fff}.dropzone .dz-preview .dz-error-message:after{content:"";position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid rgba(0,0,0,0);border-right:6px solid rgba(0,0,0,0);border-bottom:6px solid #b10606}/*# sourceMappingURL=dropzone.css.map */
-1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-14
View File
@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error - Upload Music</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css" />
</head>
<body>
<h1>Error</h1>
<p>{{ error_message }}</p>
<a href="/"><button>Upload another file</button></a>
</body>
</html>
+15 -12
View File
@@ -5,19 +5,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Music</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css" />
<script src="{{ url_for('static', filename='js/dropzone.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/dropzone.min.css') }}" type="text/css" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/dropzone.css') }}" type="text/css" />
</head>
<body>
<script>
Dropzone.options.myDropzone = {
parallelUploads: 4,
uploadMultiple: true,
acceptedFiles: 'audio/*'
};
</script>
<form action="/"
class="dropzone"
id="my-dropzone"></form>
<form action="/"
class="dropzone"
id="my-dropzone"
enctype="multipart/form-data"></form>
<script src="{{ url_for('static', filename='js/dropzone-min.js') }}"></script>
<script>
const dz = new Dropzone("#my-dropzone", {
url: "/",
parallelUploads: 4,
uploadMultiple: true,
acceptedFiles: "audio/*"
});
</script>
</body>
</html>
-14
View File
@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Success - Upload Music</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css" />
</head>
<body>
<h1>Success</h1>
<p>{{ success_message }}</p>
<a href="/"><button>Upload another file</button></a>
</body>
</html>