├── src ├── __init__.py ├── sonobarr_app │ ├── services │ │ ├── __init__.py │ │ ├── integrations │ │ │ ├── __init__.py │ │ │ ├── lastfm_user.py │ │ │ └── listenbrainz_user.py │ │ ├── releases.py │ │ └── openai_client.py │ ├── web │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── main.py │ │ ├── admin.py │ │ └── api.py │ ├── extensions.py │ ├── bootstrap.py │ ├── models.py │ ├── config.py │ ├── sockets │ │ └── __init__.py │ └── __init__.py ├── static │ ├── favicon.ico │ ├── sonobarr.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-48x48.png │ ├── favicon-64x64.png │ ├── favicon-180x180.png │ ├── favicon-192x192.png │ ├── favicon-512x512.png │ ├── style.css │ └── lidarr.svg ├── models.py ├── Sonobarr.py └── templates │ ├── login.html │ ├── admin_artist_requests.html │ ├── profile.html │ ├── admin_users.html │ ├── layout.html │ └── base.html ├── sonar-project.properties ├── migrations ├── README ├── script.py.mako ├── versions │ ├── 20251009_01_user_listening_profiles.py │ ├── 20251013_01_listenbrainz_usernames.py │ └── 20251011_01_artist_requests.py └── env.py ├── gunicorn_config.py ├── .gitignore ├── requirements.txt ├── .dockerignore ├── docker-compose.yml ├── .sample-env ├── Dockerfile ├── .github └── workflows │ ├── build.yml │ └── release-docker.yml ├── LICENSE ├── CONTRIBUTING.md ├── CHANGELOG.md ├── init.sh └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sonobarr_app/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sonobarr_app/services/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Sonobarr 2 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration for Alembic. 2 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/sonobarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/sonobarr.png -------------------------------------------------------------------------------- /src/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon-16x16.png -------------------------------------------------------------------------------- /src/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon-32x32.png -------------------------------------------------------------------------------- /src/static/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon-48x48.png -------------------------------------------------------------------------------- /src/static/favicon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon-64x64.png -------------------------------------------------------------------------------- /src/static/favicon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon-180x180.png -------------------------------------------------------------------------------- /src/static/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon-192x192.png -------------------------------------------------------------------------------- /src/static/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dodelidoo-Labs/sonobarr/HEAD/src/static/favicon-512x512.png -------------------------------------------------------------------------------- /gunicorn_config.py: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0:5000" 2 | workers = 1 3 | threads = 4 4 | timeout = 120 5 | worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" 6 | -------------------------------------------------------------------------------- /src/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from sonobarr_app.extensions import db 4 | from sonobarr_app.models import User 5 | 6 | 7 | __all__ = ["db", "User"] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | 4 | # Config files 5 | config/ 6 | 7 | # Python cache 8 | __pycache__/ 9 | *.pyc 10 | 11 | # Docker crap 12 | *.log 13 | docker-compose.override.yml 14 | 15 | VSCode 16 | .vscode/ 17 | -------------------------------------------------------------------------------- /src/Sonobarr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from sonobarr_app import create_app, socketio 4 | 5 | 6 | app = create_app() 7 | 8 | 9 | if __name__ == "__main__": 10 | socketio.run(app, host="0.0.0.0", port=5000) 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gunicorn 2 | gevent 3 | gevent-websocket 4 | flask 5 | flask_socketio 6 | flask_login 7 | flask_sqlalchemy 8 | Flask-Migrate 9 | Flask-WTF 10 | requests 11 | musicbrainzngs 12 | thefuzz 13 | Unidecode 14 | pylast 15 | openai 16 | flasgger 17 | -------------------------------------------------------------------------------- /src/sonobarr_app/web/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .auth import bp as auth_bp 4 | from .main import bp as main_bp 5 | from .admin import bp as admin_bp 6 | from .api import bp as api_bp 7 | 8 | __all__ = ["auth_bp", "main_bp", "admin_bp", "api_bp"] 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Exclude unnecessary files from Docker build context 2 | .git 3 | .github 4 | .gitignore 5 | .DS_Store 6 | README.md 7 | LICENSE 8 | *.md 9 | docker-compose.yml 10 | config 11 | *.log 12 | __pycache__ 13 | *.pyc 14 | *.pyo 15 | *.pyd 16 | .pytest_cache 17 | venv 18 | env 19 | .env 20 | .sample-env 21 | tests 22 | tmp 23 | node_modules 24 | -------------------------------------------------------------------------------- /src/sonobarr_app/extensions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from flask_login import LoginManager 4 | from flask_migrate import Migrate 5 | from flask_socketio import SocketIO 6 | from flask_sqlalchemy import SQLAlchemy 7 | from flask_wtf import CSRFProtect 8 | 9 | 10 | db = SQLAlchemy() 11 | login_manager = LoginManager() 12 | socketio = SocketIO() 13 | csrf = CSRFProtect() 14 | migrate = Migrate() 15 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message}""" 2 | 3 | revision = ${repr(revision)} 4 | down_revision = ${repr(down_revision)} 5 | branch_labels = ${repr(branch_labels)} 6 | depends_on = ${repr(depends_on)} 7 | 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | def upgrade(): 13 | ${upgrades if upgrades else "pass"} 14 | 15 | 16 | def downgrade(): 17 | ${downgrades if downgrades else "pass"} 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sonobarr: 3 | image: ghcr.io/dodelidoo-labs/sonobarr:latest # pin to ghcr.io/dodelidoo-labs/sonobarr:0.7.0 for a specific release 4 | container_name: sonobarr 5 | env_file: 6 | - .env 7 | volumes: 8 | - ./config:/sonobarr/config 9 | - /etc/localtime:/etc/localtime:ro 10 | ports: 11 | - "5000:5000" 12 | restart: unless-stopped 13 | 14 | # Need to attach Sonobarr to an external reverse-proxy network instead of 15 | # publishing port 5000? Remove the `ports:` section above and add a `networks:` 16 | # stanza that matches your proxy. See the README for details. 17 | -------------------------------------------------------------------------------- /.sample-env: -------------------------------------------------------------------------------- 1 | secret_key= 2 | 3 | # Lidarr configuration 4 | lidarr_address=http://192.168.1.1:8686 5 | lidarr_api_key= 6 | root_folder_path=/data/media/music/ 7 | quality_profile_id=1 8 | metadata_profile_id=1 9 | lidarr_api_timeout=120 10 | fallback_to_top_result=false 11 | search_for_missing_albums=false 12 | dry_run_adding_to_lidarr=false 13 | 14 | # Discovery tuning 15 | similar_artist_batch_size=10 16 | auto_start=false 17 | auto_start_delay=60 18 | 19 | # External APIs 20 | last_fm_api_key= 21 | last_fm_api_secret= 22 | youtube_api_key= 23 | openai_api_key= 24 | openai_model=gpt-4o-mini 25 | openai_max_seed_artists=5 26 | 27 | # Super-admin bootstrap (used on first launch) 28 | sonobarr_superadmin_username=admin 29 | sonobarr_superadmin_password=change-me 30 | sonobarr_superadmin_display_name=Super Admin 31 | sonobarr_superadmin_reset=false 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine 2 | 3 | ARG RELEASE_VERSION 4 | ENV RELEASE_VERSION=${RELEASE_VERSION} 5 | ENV PYTHONPATH="/sonobarr/src" 6 | 7 | RUN apk update && apk add --no-cache su-exec \ 8 | && addgroup -S -g 1000 sonobarr \ 9 | && adduser -S -G sonobarr -u 1000 sonobarr 10 | 11 | # Copy only requirements first 12 | COPY requirements.txt /sonobarr/ 13 | WORKDIR /sonobarr 14 | 15 | # Install requirements 16 | RUN pip install --no-cache-dir -r requirements.txt 17 | 18 | 19 | # Now copy the rest of your code 20 | COPY src/ /sonobarr/src/ 21 | COPY migrations/ /sonobarr/migrations/ 22 | COPY gunicorn_config.py /sonobarr/ 23 | COPY init.sh /sonobarr/ 24 | 25 | RUN chmod 755 init.sh \ 26 | && mkdir -p /sonobarr/config \ 27 | && chown -R sonobarr:sonobarr /sonobarr/config 28 | 29 | USER sonobarr 30 | 31 | ENTRYPOINT ["./init.sh"] 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | 9 | jobs: 10 | build: 11 | name: Build and analyze 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 18 | - uses: SonarSource/sonarqube-scan-action@v6 19 | env: 20 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 21 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 22 | # If you wish to fail your job when the Quality Gate is red, uncomment the 23 | # following lines. This would typically be used to fail a deployment. 24 | # - uses: SonarSource/sonarqube-quality-gate-action@v1 25 | # timeout-minutes: 5 26 | # env: 27 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TheWicklowWolf 4 | Copyright (c) 2025 Dodelidoo Labs 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /migrations/versions/20251009_01_user_listening_profiles.py: -------------------------------------------------------------------------------- 1 | """add Last.fm username column 2 | 3 | Revision ID: 20251009_01 4 | Revises: 5 | Create Date: 2025-10-09 12:00:00 6 | """ 7 | 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import inspect 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "20251009_01" 15 | down_revision = None 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | bind = op.get_bind() 22 | inspector = inspect(bind) 23 | existing_columns = {col["name"] for col in inspector.get_columns("users")} 24 | 25 | with op.batch_alter_table("users", schema=None) as batch_op: 26 | if "lastfm_username" not in existing_columns: 27 | batch_op.add_column( 28 | sa.Column("lastfm_username", sa.String(length=120), nullable=True) 29 | ) 30 | 31 | 32 | def downgrade(): 33 | bind = op.get_bind() 34 | inspector = inspect(bind) 35 | existing_columns = {col["name"] for col in inspector.get_columns("users")} 36 | 37 | with op.batch_alter_table("users", schema=None) as batch_op: 38 | if "lastfm_username" in existing_columns: 39 | batch_op.drop_column("lastfm_username") 40 | -------------------------------------------------------------------------------- /migrations/versions/20251013_01_listenbrainz_usernames.py: -------------------------------------------------------------------------------- 1 | """add ListenBrainz username column 2 | 3 | Revision ID: 20251013_01 4 | Revises: 20251011_01 5 | Create Date: 2025-10-13 00:00:00 6 | """ 7 | 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy import inspect 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "20251013_01" 15 | down_revision = "20251011_01" 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | bind = op.get_bind() 22 | inspector = inspect(bind) 23 | existing_columns = {column["name"] for column in inspector.get_columns("users")} 24 | 25 | if "listenbrainz_username" not in existing_columns: 26 | with op.batch_alter_table("users", schema=None) as batch_op: 27 | batch_op.add_column(sa.Column("listenbrainz_username", sa.String(length=120), nullable=True)) 28 | 29 | 30 | def downgrade(): 31 | bind = op.get_bind() 32 | inspector = inspect(bind) 33 | existing_columns = {column["name"] for column in inspector.get_columns("users")} 34 | 35 | if "listenbrainz_username" in existing_columns: 36 | with op.batch_alter_table("users", schema=None) as batch_op: 37 | batch_op.drop_column("listenbrainz_username") 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Clone the repository. 4 | 2. Checkout the `develop` branch: `git checkout develop`. 5 | 3. **Create** a `docker-compose.override.yml` with the following contents: 6 | ```yaml 7 | services: 8 | sonobarr: 9 | build: 10 | context: . 11 | dockerfile: Dockerfile 12 | network: host 13 | image: sonobarr-local 14 | volumes: 15 | - ./config:/sonobarr/config 16 | - /etc/localtime:/etc/localtime:ro 17 | - ./src:/sonobarr/src 18 | #ports: 19 | # - "5000:5000" 20 | networks: 21 | npm_proxy: 22 | ipv4_address: 192.168.97.23 #change as you need 23 | 24 | networks: 25 | npm_proxy: 26 | external: true 27 | ``` 28 | 4. Build the image with `sudo docker compose up -d` - later this will re-use the local image. 29 | 5. Make code changes in `src/` or other required files. 30 | 6. Test the changes by restarting the docker image `sudo docker compose down && sudo docker compose up -d` and clearing cache in browser. 31 | 7. Once ready to commit, make sure the build still works as well `sudo docker compose down -v --remove-orphans && sudo docker system prune -a --volumes -f && sudo docker compose up -d`. 32 | 8. Commit your work to the `develop` branch. 33 | 34 | **Always test your changes with at least two accounts - admin and a common user - in the app, in at least two distinct browser builds (such as safari and chrome, for example).** 35 | **Remember that if you made changes affecting config (that is, database or configuration) you have to delete the `./config` folder before rebuilding or restarting the app. 36 | -------------------------------------------------------------------------------- /src/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Sign in · Sonobarr{% endblock %} 4 | {% block body_classes %}bg-body-secondary{% endblock %} 5 | {% block topbar_title %}Sign in{% endblock %} 6 | {% block topbar_actions %}{% endblock %} 7 | {% block header %}{% endblock %} 8 | 9 | {% block main %} 10 |
18 | 33 | Need help? Ask your Sonobarr administrator to create an account for you. 34 |
35 || Artist | 23 |Requested by | 24 |Requested at | 25 |Actions | 26 |
|---|---|---|---|
| {{ req.artist_name }} | 32 |{{ req.requested_by.name }} | 33 |{{ req.created_at.strftime('%Y-%m-%d %H:%M') }} | 34 |35 | 41 | 47 | | 48 |
No pending requests.
55 | {% endif %} 56 || Username | 63 |Display name | 64 |Role | 65 |Actions | 66 |
|---|---|---|---|
| {{ user.username }} | 72 |{{ user.display_name or '—' }} | 73 |74 | {% if user.is_admin %} 75 | Admin 76 | {% else %} 77 | User 78 | {% endif %} 79 | | 80 |81 | {% if user.id != current_user.id %} 82 | 88 | {% else %} 89 | This is you 90 | {% endif %} 91 | | 92 |
No users found.
99 | {% endif %} 100 |
12 |
13 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |