fix: sync album list record name when title is updated (#551)

When an album title is updated via PATCH /albums/{album_id}, we now also
update the album's ATProto list record with the new name. Previously only
track records were updated, leaving the list record with the stale name.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub dbf571ad 6aca63c4

Changed files
+57 -12
backend
src
backend
api
tests
+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
··· 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(