20 Commits

Author SHA1 Message Date
arian 307de5fc94 docs(changelog): v0.3.0 changelog
Build and Test Debian Package / build (push) Successful in 7s
Build and Test Debian Package / test (push) Successful in 5s
2026-05-31 08:44:46 -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
17 changed files with 218 additions and 47 deletions
+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
+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"]
+17
View File
@@ -1,3 +1,20 @@
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:
+1 -1
View File
@@ -7,6 +7,6 @@ Standards-Version: 4.7.0
Rules-Requires-Root: no
Package: navidrome-uploader
Architecture: all
Architecture: amd64
Depends: ${misc:Depends}, adduser, python3, python3-venv, python3-pip, python3-wheel
Description: Navidrome Web Upload Utility
+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
+22
View File
@@ -0,0 +1,22 @@
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)
+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')
+5 -3
View File
@@ -3,7 +3,7 @@
# 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')
@@ -25,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
+5 -1
View File
@@ -8,4 +8,8 @@ cd "${PROJECT_ROOT}"
docker build --platform linux/amd64 -t uploader-builder -f Dockerfile.build .
docker run --rm --platform linux/amd64 -v "$(pwd)/output:/dist" uploader-builder
docker run --name uploader-builder-container --platform linux/amd64 uploader-builder
docker cp uploader-builder-container:/dist/. output/
docker rm uploader-builder-container
+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>