Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
e4f6d82
dev: ruff.
2e0byo Dec 28, 2025
43cc020
chore: run `ruff format`.
2e0byo Dec 28, 2025
02debc7
refactor: run ruff check --fix and clean up mocking.
2e0byo Dec 28, 2025
74fb88b
refactor: remaining lints.
2e0byo Dec 28, 2025
751df77
chore: vendor ruff.
2e0byo Dec 28, 2025
341e726
chore: add ruff format --check to lint target.
2e0byo Dec 28, 2025
298f80f
chore: format.
2e0byo Dec 28, 2025
22b09c4
dev: port to uv + set up for nix.
2e0byo Dec 28, 2025
2d9d5b6
chore: remove upper bound constraints.
2e0byo Dec 28, 2025
29c14a2
chore: update makefile to uv.
2e0byo Dec 28, 2025
f229bfb
ci: port to uv.
2e0byo Dec 28, 2025
9349a05
feat: simple http proxy cache.
2e0byo Dec 28, 2025
859fcf9
chore: bump python to at least 3.12.
2e0byo Dec 28, 2025
1aa26d9
chore: migrate to pytest-cases.
2e0byo Dec 28, 2025
35e9b7d
refactor: break out httpserver.
2e0byo Dec 28, 2025
30e8319
test: https.
2e0byo Dec 28, 2025
82762ed
refactor: break out track stream creation.
2e0byo Dec 29, 2025
c2f2217
refactor: type-safe port transform.
2e0byo Dec 29, 2025
a386b59
feat: calculate local url on proxy config.
2e0byo Dec 29, 2025
250ce73
feat(proxy): standardise path.
2e0byo Dec 29, 2025
c0c5392
feat: use proxy for playback.
2e0byo Dec 29, 2025
3052c3e
feat(cache): shutdown thread on exit.
2e0byo Dec 29, 2025
7d9e4c9
docs: ssl_context.
2e0byo Dec 29, 2025
78a5a0d
fix(proxy): name logger.
2e0byo Dec 29, 2025
060da05
fix: remember to correct the Host: line...
2e0byo Dec 29, 2025
47cdceb
chore: keep linter happy (header is pretty small, so who cares).
2e0byo Dec 29, 2025
47cb097
feat: better check_status.
2e0byo Dec 29, 2025
4ac3f5a
feat: process proxy.
2e0byo Dec 29, 2025
24910bc
fix: tidal proxy url.
2e0byo Dec 29, 2025
f2fc63d
fix: gstreamer holds the GIL, so we need a process or we deadlock.
2e0byo Dec 29, 2025
c8caf9f
feat: start moving to domain types.
2e0byo Dec 29, 2025
38e38b9
chore: dead code.
2e0byo Dec 29, 2025
547117f
fix: close when all data sent, not when partial sent.
2e0byo Dec 29, 2025
b78260e
fix: conditional finalisation.
2e0byo Dec 29, 2025
f8e37eb
fix: avoid cache busting every time the token changes.
2e0byo Dec 29, 2025
4c97a86
fix: errant uniques
2e0byo Dec 29, 2025
b4dc5e7
refactor: impl cache_key for clarity.
2e0byo Dec 29, 2025
b929215
refactor: offest and read are the same.
2e0byo Dec 29, 2025
56d1f71
feat: separate read chunks from buffer slices.
2e0byo Dec 29, 2025
21b3657
fix: just write the whole chunk.
2e0byo Dec 29, 2025
4da1314
fix: avoid repeating first chunk.
2e0byo Dec 29, 2025
7daf249
fix: need to use process isolation to prevent gstreamer stalling.
2e0byo Dec 29, 2025
f672824
fix: ignore errors if the other side has closed.
2e0byo Dec 29, 2025
e19ef01
fix: handle open ranges.
2e0byo Dec 29, 2025
029dd89
feat: proper buffer type.
2e0byo Dec 29, 2025
3df947d
chore: better logging.
2e0byo Dec 29, 2025
418bd3e
chore: deps inc for testing.
2e0byo Dec 29, 2025
b3d099e
chore(nix): pull in mopidy itself for its build deps.
2e0byo Dec 30, 2025
3aff8f2
chore: mpd for tests.
2e0byo Dec 30, 2025
135281e
chore(nix): integration test deps.
2e0byo Dec 30, 2025
643534f
test: bring back integration tests for config.
2e0byo Dec 30, 2025
703d49d
test: bring back login hack integration tests.
2e0byo Dec 30, 2025
44f2e4f
ci: port integration tests to uv.
2e0byo Dec 30, 2025
daaa7c2
ci: disable schedule temporarily to try to enable.
2e0byo Dec 30, 2025
1df0878
ci: new name to try to enable
2e0byo Dec 30, 2025
1581983
ci: fix.
2e0byo Dec 30, 2025
8fda933
ci: fix deps.
2e0byo Dec 30, 2025
7d1c4a7
ci: try system venv.
2e0byo Dec 30, 2025
cefff78
ci: last shot with compiling...
2e0byo Dec 30, 2025
ad197c3
ci: fix versions.
2e0byo Dec 30, 2025
f52cb57
ci: avoid cache collision.
2e0byo Dec 30, 2025
5500f6f
chore: require python>=3.13 for latest mopidy.
2e0byo Dec 30, 2025
9323e1a
feat: try mopidy's apt... starting to wonder about nix :D
2e0byo Dec 30, 2025
cb6f345
ci: disable git mopidy for now.
2e0byo Dec 30, 2025
5c1be21
fix: .index() raises rather than returning -1.
2e0byo Dec 30, 2025
4ed0b41
feat: parse out keep-alive if present.
2e0byo Dec 30, 2025
cc0dfe6
fix: request was parsing the *response* keep alive header...
2e0byo Dec 30, 2025
bb50917
test: cache inc concurrency.
2e0byo Dec 31, 2025
cfe29ec
feat(sql): add field for LRU.
2e0byo Jan 1, 2026
4e18479
fix(sql): concurrency.
2e0byo Jan 1, 2026
20e0fe7
fix: construct cache exactly once.
2e0byo Jan 1, 2026
df1d970
chore: assert.
2e0byo Jan 1, 2026
0e058e7
test: long insertions in cache.
2e0byo Jan 1, 2026
26accd1
fix: ensure we only ever return one entry.
2e0byo Jan 1, 2026
8a6d423
chore: debug logging.
2e0byo Jan 1, 2026
fa63657
fix: throw away partial requests.
2e0byo Jan 1, 2026
fe5fd79
chore: tidy.
2e0byo Jan 1, 2026
3af5a08
chore: log config.
2e0byo Jan 1, 2026
dd4e3ab
chore: cleanup.
2e0byo Jan 1, 2026
7fb9864
chore: log range as well when serving.
2e0byo Jan 1, 2026
fbd1dfb
fix: ignore connection errors explicitly.
2e0byo Jan 1, 2026
1eac310
chore: keep linter happy.
2e0byo Jan 1, 2026
b16e857
chore: tidy.
2e0byo Jan 1, 2026
935c136
feat: try to prevent partial insertions from surviving...
2e0byo Jan 1, 2026
f699440
chore: remove dev test.
2e0byo Jan 1, 2026
45795fa
test: check headers.
2e0byo Jan 1, 2026
0adc192
fix: http uses closed ranges.
2e0byo Jan 1, 2026
86a0b39
fix: ranges.
2e0byo Jan 1, 2026
8c7786b
test: last byte range.
2e0byo Jan 1, 2026
87c4ae5
test: be explicit about headers.
2e0byo Jan 1, 2026
c0e4157
feat: honour keep-alive.
2e0byo Jan 1, 2026
b7ee78d
test(sqlite cache): incomplete insertions discareded.
2e0byo Jan 2, 2026
e50d7da
test: mixed incomplete and complete insertions.
2e0byo Jan 2, 2026
9d727a8
fix: incomplete + complete insertions.
2e0byo Jan 2, 2026
4737459
chore: no special config needed now.
2e0byo Jan 2, 2026
e8521e0
fix: timeout on connection opening.
2e0byo Jan 2, 2026
b7fd5dd
chore: remove dead code.
2e0byo Jan 2, 2026
1dc2d44
perf: improve buffer.
2e0byo Jan 2, 2026
ad29875
chore: lint.
2e0byo Jan 2, 2026
adeec90
fix: timeout all socket reads and writes; close sockets on error.
2e0byo Jan 2, 2026
5347ae0
feat: keep a handle on server so it doesn't get GCd.
2e0byo Jan 2, 2026
a50d4e6
fix: case-insensitive header handling.
2e0byo Jan 2, 2026
1cc8738
chore: placate linter.
2e0byo Jan 2, 2026
070c2c7
chore: lint
2e0byo Jan 3, 2026
0c8db50
feat(sql): body.entry_id is a foreign key.
2e0byo Jan 3, 2026
be4fe01
perf: update indices.
2e0byo Jan 3, 2026
5189bc1
feat: version db.
2e0byo Jan 3, 2026
f9c9006
feat(sql): store time as unix epoch and update it.
2e0byo Jan 3, 2026
eb72bf5
test: cache updating.
2e0byo Jan 3, 2026
870d72b
refactor: we're already inside a context manager.
2e0byo Jan 3, 2026
4357fea
chore: add cleanup to cache init.
2e0byo Jan 3, 2026
64eec99
refactor: make sql SHOUTY.
2e0byo Jan 3, 2026
935f655
feat: cache eviction (LRU).
2e0byo Jan 3, 2026
66b895a
refactor: improve naming.
2e0byo Jan 4, 2026
e162a2d
chore: explain dict cache.
2e0byo Jan 4, 2026
1b0acc5
chore: standardise naming.
2e0byo Jan 4, 2026
14edbf4
feat(sql): add not null constraints.
2e0byo Jan 4, 2026
67aac00
feat: add manual flag for offline content.
2e0byo Jan 4, 2026
bd991a6
feat: add table for tidal id -> path.
2e0byo Jan 4, 2026
5fe20f2
chore: sort out case.
2e0byo Jan 4, 2026
d5f5b47
fix: turn on foreign keys.
2e0byo Jan 4, 2026
f2f2448
fix: unique constraint on entry_id in head.
2e0byo Jan 4, 2026
47f590b
perf: idx on entry_id in path.
2e0byo Jan 4, 2026
524205b
chore: cascade does the delete already.
2e0byo Jan 4, 2026
2106f81
feat: path lookup table.
2e0byo Jan 4, 2026
c90443f
fix: make sure to enable foreign keys even if not calling init().
2e0byo Jan 4, 2026
4d76db3
feat: expose cache on proxy.
2e0byo Jan 4, 2026
d47a556
feat: use cached uri resolution where possible.
2e0byo Jan 4, 2026
5bcaf20
feat: it seems we can use the threaded cache after all.
2e0byo Jan 4, 2026
78662b3
chore: lint.
2e0byo Jan 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
35 changes: 19 additions & 16 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Integration Test

on:
schedule:
- cron: "0 0 * * 0" # weekly
# schedule:
# - cron: "0 0 * * 0" # weekly
push:
pull_request:
types: [opened, synchronize]
Expand All @@ -13,33 +13,36 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, "3.10", 3.11]
matrix:
python-version: [3.12, 3.13, 3.14]
mopidy-version: [3.3, 3.4, "git"]
# Run all the matrix jobs, even if one fails.
fail-fast: false

steps:
- uses: actions/checkout@v3
- name: Install Mopidy system-wide
run: sudo apt update && sudo apt install mopidy libgirepository1.0-dev mpc
- name: Install Poetry
uses: abatilo/actions-poetry@v2
- name: Set up Python
uses: actions/setup-python@v4
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
enable-cache: true
cache-suffix: ${{ matrix.python-version }}

- name: Install Mopidy system-wide
run: sudo apt update && sudo apt install mopidy libgirepository1.0-dev mpc

- name: Correct mopidy version
run: |
if [ ${{ matrix.mopidy-version }} != "git" ]; then
poetry add mopidy@${{ matrix.mopidy-version }}
uv add mopidy==${{ matrix.mopidy-version }}
else
poetry add "git+https://github.com/mopidy/mopidy.git"
uv add "mopidy @ git+https://github.com/mopidy/mopidy.git"
fi
poetry show
uv pip show mopidy
- name: Install mopidy-tidal
run: |
rm poetry.lock
poetry install --with complete
uv sync --group test

- name: Integration test
run: make integration-test
61 changes: 61 additions & 0 deletions .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Integration Test

on:
schedule:
- cron: "0 0 * * 0" # weekly
push:
pull_request:
types: [opened, synchronize]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
# TODO bring back 3.12 for non-git. Should expand the cartesian product ourselves.
python-version: [3.13, 3.14]
mopidy-version: [3.3, 3.4] #, "git"]
# Run all the matrix jobs, even if one fails.
fail-fast: false

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: ${{ matrix.python-version }}-${{ matrix.mopidy-version }}

- name: Install Mopidy system-wide
run: |
sudo apt update
sudo apt install mopidy libgirepository1.0-dev mpc \
build-essential python3-dev python3-pip \
gir1.2-gst-plugins-base-1.0 \
gir1.2-gstreamer-1.0 \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-ugly \
gstreamer1.0-tools \
libcairo2-dev \
libgirepository-2.0-dev \
python3-gst-1.0



- name: Correct mopidy version
run: |
if [ ${{ matrix.mopidy-version }} != "git" ]; then
uv add mopidy==${{ matrix.mopidy-version }}
else
uv add "mopidy @ git+https://github.com/mopidy/mopidy.git"
fi
uv pip show mopidy
- name: Install mopidy-tidal
run: |
uv sync --group complete

- name: Integration test
run: make integration-test
22 changes: 13 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,34 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, "3.10", 3.11]
python-version: [3.13, 3.14]
# Run all the matrix jobs, even if one fails.
fail-fast: false

steps:
- uses: actions/checkout@v3
- name: Install Poetry
uses: abatilo/actions-poetry@v2
- name: Set up Python
uses: actions/setup-python@v4
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
enable-cache: true
cache-suffix: ${{ matrix.python-version }}

- name: Install mopidy-tidal
run: |
poetry install
uv sync

- name: Lint
run: |
make lint

- name: Test
run: |
make test

- name: Upload coverage
if: ${{ matrix.python-version == '3.10' }}
if: ${{ matrix.python-version == '3.12' }}
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,4 @@ poetry.toml
pyrightconfig.json

# End of https://www.toptal.com/developers/gitignore/api/python
/.direnv/
11 changes: 7 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ something is supposed to work is probably to have a look at the tests.


### Code Style
Code should be formatted with `isort` and `black`:
Code should be formatted with `ruff`:

```bash
isort --profile=black mopidy_tidal tests
black mopidy_tidal tests
ruff format
```

Additionally, run `ruff check` and clean up or `#noqa` its suggestions. You may
want to use `ruff check --fix`, potentially after staging so you can see the
difference.

if you are on *nix you can run:

```bash
Expand Down Expand Up @@ -107,4 +110,4 @@ by design of entering the venv permanently from within the makefile.)


In either case, run `mopidy` inside the virtualenv to launch mopidy with your
development version of Mopidy-Tidal.
development version of Mopidy-Tidal.
61 changes: 61 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
description = "gstreamer proxy";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};

outputs = {
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem
(
system: let
pkgs = import nixpkgs {
inherit system;
};
python = pkgs.python313;
buildInputs =
(with pkgs; [
(python.withPackages (ps:
with ps; [
gst-python
pygobject3
]))
uv
pre-commit
ruff
mopidy # for its build inputs: it would be nice to do this properly, but I can't seem to get network playing to work
gobject-introspection
mpc # integration tests
])
++ (with pkgs.gst_all_1; [
pkgs.glib-networking
gst-plugins-bad
gst-plugins-base
gst-plugins-good
gst-plugins-ugly
gst-plugins-rs
]);
env = {
UV_PROJECT_ENVIRONMENT = ".direnv/venv";
};
in
with pkgs; {
devShells.default = mkShell {
inherit buildInputs;
inherit env;
shellHook = ''
# pre-commit install
[ ! -d $UV_PROJECT_ENVIRONMENT ] && uv venv $UV_PROJECT_ENVIRONMENT --python ${python}/bin/python
source $UV_PROJECT_ENVIRONMENT/bin/activate
'';
};
}
);
}
Empty file added integration_tests/__init__.py
Empty file.
3 changes: 2 additions & 1 deletion integration_tests/config/basic.conf
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[tidal]
quality=LOSSLESS
quality=LOSSLESS
login_method=BLOCK
42 changes: 26 additions & 16 deletions integration_tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
from pathlib import Path

import pytest

from .util import mopidy


def test_basic_config_loads_tidal_generates_auth_url(spawn, config_dir):
config = config_dir / "basic.conf"
with spawn(f"mopidy --config {config.resolve()}") as child:
child.expect("Connecting to TIDAL... Quality = LOSSLESS")
child.expect("Visit https://link.tidal.com/.* to log in")
def test_basic_config_loads_tidal_generates_auth_url(
spawn, config_dir: Path, tmp_path: Path
):
with spawn(mopidy(config_dir, "basic.conf", tmp_path)) as child:
child.expect("Quality: LOSSLESS")
child.expect("Authentication: OAuth")
child.expect("Please visit.*https://link.tidal.com/.* to authenticate")


def test_lazy_config_no_connect_to_tidal(spawn, config_dir):
config = config_dir / "lazy.conf"
with spawn(f"mopidy --config {config.resolve()}") as child:
child.expect("Connecting to TIDAL... Quality = LOSSLESS")
def test_lazy_config_no_connect_to_tidal(spawn, config_dir: Path, tmp_path: Path):
with spawn(mopidy(config_dir, "lazy.conf", tmp_path)) as child:
child.expect("Quality: LOSSLESS")
child.expect("Authentication: OAuth")
with pytest.raises(AssertionError):
child.expect("Visit https://link.tidal.com/.* to log in")
child.expect("Please visit.*https://link.tidal.com/.* to authenticate")


def test_lazy_config_generates_auth_url_on_access(spawn, config_dir):
config = config_dir / "lazy.conf"
with spawn(f"mopidy --config {config.resolve()}") as child:
child.expect("Connecting to TIDAL... Quality = LOSSLESS")
def test_lazy_config_generates_auth_url_on_access(
spawn, config_dir: Path, tmp_path: Path
):
with spawn(mopidy(config_dir, "lazy.conf", tmp_path)) as child:
child.expect("Quality: LOSSLESS")
child.expect("Authentication: OAuth")

with pytest.raises(AssertionError):
child.expect("Visit https://link.tidal.com/.* to log in")
child.expect("Please visit.*https://link.tidal.com/.* to authenticate")

with spawn("mpc list artist"):
child.expect("Visit https://link.tidal.com/.* to log in")
child.expect("Not logged in.* visit https://link.tidal.com/.*")
Loading