+21
-2
backend/src/backend/api/albums.py
+21
-2
backend/src/backend/api/albums.py
···
472
) -> AlbumMetadata:
473
"""update album metadata (title, description).
474
475
-
when title changes, all tracks in the album have their ATProto records
476
-
updated to reflect the new album name.
477
"""
478
from backend._internal.atproto.records.fm_plyr.track import (
479
build_track_record,
480
update_record,
···
529
record=updated_record,
530
)
531
track.atproto_record_cid = new_cid
532
533
await db.commit()
534
···
472
) -> AlbumMetadata:
473
"""update album metadata (title, description).
474
475
+
when title changes:
476
+
- all tracks in the album have their ATProto records updated
477
+
- the album's ATProto list record name is updated
478
"""
479
+
from backend._internal.atproto.records.fm_plyr.list import update_list_record
480
from backend._internal.atproto.records.fm_plyr.track import (
481
build_track_record,
482
update_record,
···
531
record=updated_record,
532
)
533
track.atproto_record_cid = new_cid
534
+
535
+
# update the album's ATProto list record name
536
+
if album.atproto_record_uri:
537
+
track_refs = [
538
+
{"uri": t.atproto_record_uri, "cid": t.atproto_record_cid}
539
+
for t in album.tracks
540
+
if t.atproto_record_uri and t.atproto_record_cid
541
+
]
542
+
_, new_list_cid = await update_list_record(
543
+
auth_session=auth_session,
544
+
list_uri=album.atproto_record_uri,
545
+
items=track_refs,
546
+
name=new_title,
547
+
list_type="album",
548
+
created_at=album.created_at,
549
+
)
550
+
album.atproto_record_cid = new_list_cid
551
552
await db.commit()
553
+36
-10
backend/tests/api/test_albums.py
+36
-10
backend/tests/api/test_albums.py
···
631
1. album title is updated in database
632
2. track extra["album"] is updated for all tracks
633
3. ATProto records are updated for tracks that have them
634
"""
635
# create artist matching mock session
636
artist = Artist(
···
641
db_session.add(artist)
642
await db_session.flush()
643
644
-
# create album
645
album = Album(
646
artist_did=artist.did,
647
slug="test-album",
648
title="Original Title",
649
)
650
db_session.add(album)
651
await db_session.flush()
···
667
album_id = album.id
668
track_id = track.id
669
670
-
# mock ATProto update_record
671
-
with patch(
672
-
"backend._internal.atproto.records.fm_plyr.track.update_record",
673
-
new_callable=AsyncMock,
674
-
return_value=("at://did:test:user123/fm.plyr.track/track123", "new_cid"),
675
-
) as mock_update:
676
async with AsyncClient(
677
transport=ASGITransport(app=test_app), base_url="http://test"
678
) as client:
···
683
assert data["title"] == "Updated Title"
684
assert data["id"] == album_id
685
686
-
# verify ATProto update was called
687
-
mock_update.assert_called_once()
688
-
call_kwargs = mock_update.call_args.kwargs
689
assert call_kwargs["record"]["album"] == "Updated Title"
690
691
# verify track extra["album"] was updated in database
692
from backend.utilities.database import get_engine
693
···
697
updated_track = result.scalar_one()
698
assert updated_track.extra["album"] == "Updated Title"
699
assert updated_track.atproto_record_cid == "new_cid"
700
701
702
async def test_update_album_forbidden_for_non_owner(
···
631
1. album title is updated in database
632
2. track extra["album"] is updated for all tracks
633
3. ATProto records are updated for tracks that have them
634
+
4. album's ATProto list record name is updated
635
"""
636
# create artist matching mock session
637
artist = Artist(
···
642
db_session.add(artist)
643
await db_session.flush()
644
645
+
# create album with ATProto list record
646
album = Album(
647
artist_did=artist.did,
648
slug="test-album",
649
title="Original Title",
650
+
atproto_record_uri="at://did:test:user123/fm.plyr.dev.list/album123",
651
+
atproto_record_cid="original_list_cid",
652
)
653
db_session.add(album)
654
await db_session.flush()
···
670
album_id = album.id
671
track_id = track.id
672
673
+
# mock ATProto update_record for tracks and list
674
+
with (
675
+
patch(
676
+
"backend._internal.atproto.records.fm_plyr.track.update_record",
677
+
new_callable=AsyncMock,
678
+
return_value=("at://did:test:user123/fm.plyr.track/track123", "new_cid"),
679
+
) as mock_track_update,
680
+
patch(
681
+
"backend._internal.atproto.records.fm_plyr.list.update_list_record",
682
+
new_callable=AsyncMock,
683
+
return_value=(
684
+
"at://did:test:user123/fm.plyr.dev.list/album123",
685
+
"new_list_cid",
686
+
),
687
+
) as mock_list_update,
688
+
):
689
async with AsyncClient(
690
transport=ASGITransport(app=test_app), base_url="http://test"
691
) as client:
···
696
assert data["title"] == "Updated Title"
697
assert data["id"] == album_id
698
699
+
# verify track ATProto update was called
700
+
mock_track_update.assert_called_once()
701
+
call_kwargs = mock_track_update.call_args.kwargs
702
assert call_kwargs["record"]["album"] == "Updated Title"
703
704
+
# verify list record update was called with new name
705
+
mock_list_update.assert_called_once()
706
+
list_call_kwargs = mock_list_update.call_args.kwargs
707
+
assert list_call_kwargs["name"] == "Updated Title"
708
+
assert list_call_kwargs["list_type"] == "album"
709
+
710
# verify track extra["album"] was updated in database
711
from backend.utilities.database import get_engine
712
···
716
updated_track = result.scalar_one()
717
assert updated_track.extra["album"] == "Updated Title"
718
assert updated_track.atproto_record_cid == "new_cid"
719
+
720
+
# verify album list record CID was updated
721
+
album_result = await fresh_session.execute(
722
+
select(Album).where(Album.id == album_id)
723
+
)
724
+
updated_album = album_result.scalar_one()
725
+
assert updated_album.atproto_record_cid == "new_list_cid"
726
727
728
async def test_update_album_forbidden_for_non_owner(