diff --git a/docs/general/data-update-intervals.rst b/docs/general/data-update-intervals.rst index bb9006df99..91b90bab10 100644 --- a/docs/general/data-update-intervals.rst +++ b/docs/general/data-update-intervals.rst @@ -7,13 +7,14 @@ Expected schedule: System Update schedule =============================================== ========================================= Receiving listens, updating listen counts Immediate [#f1]_ -Deleting listens Removed at the top of the next hour (UTC) +Deleting listens Removed at the top of the next hour (UTC) Updating statistics for new listens Daily [#f2]_ Removing deleted listens from stats On the 2nd and 16th of each month -Full dumps 1st and 15th of each month -Incremental dumps Daily -Weekly playlists Monday morning, based on the users timezone setting -Daily playlists [#f3]_ Every morning, based on the users timezone setting +Full dumps 1st and 15th of each month +Incremental dumps Daily +Link listens Monday morning at 2AM (UTC) +Weekly playlists Monday morning, based on the user's timezone setting +Daily playlists [#f3]_ Every morning, based on the user's timezone setting =============================================== ========================================= Situations will occasionally arise where these take longer. If you have been a very patient user, and diff --git a/frontend/js/src/settings/link-listens/LinkListens.tsx b/frontend/js/src/settings/link-listens/LinkListens.tsx index c3ccded310..de5e2b0341 100644 --- a/frontend/js/src/settings/link-listens/LinkListens.tsx +++ b/frontend/js/src/settings/link-listens/LinkListens.tsx @@ -13,7 +13,7 @@ import { Helmet } from "react-helmet"; import NiceModal from "@ebay/nice-modal-react"; -import { groupBy, isNil, isNull, pick, size, sortBy } from "lodash"; +import { groupBy, isNil, isNull, isString, pick, size, sortBy } from "lodash"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery } from "@tanstack/react-query"; import ReactTooltip from "react-tooltip"; @@ -86,6 +86,15 @@ export default function LinkListensPage() { const [searchParams, setSearchParams] = useSearchParams(); const pageSearchParam = searchParams.get("page"); + const lastUpdatedHumanReadable = isString(lastUpdated) + ? new Date(lastUpdated).toLocaleString(undefined, { + day: "2-digit", + month: "short", + hour: "numeric", + minute: "numeric", + hour12: true, + }) + : "—"; // State const [deletedListens, setDeletedListens] = React.useState>([]); const [unlinkedListens, setUnlinkedListens] = React.useState< @@ -249,8 +258,7 @@ export default function LinkListensPage() { first.

- You will find below your top 1000 listens (grouped by album) that - have  + Your top 1,000 listens (grouped by album) that have  not been automatically linked -   to a MusicBrainz recording. Link them below or  - - submit new data to MusicBrainz - - . +  to a MusicBrainz recording.

MusicBrainz is the open-source - music encyclopedia that ListenBrainz uses to display more information - about your music. + music encyclopedia that ListenBrainz uses to display information about + your music.  + + Submit missing data to MusicBrainz + + .

{!isNil(lastUpdated) && ( -

Last updated {new Date(lastUpdated).toLocaleDateString()}

+

+ Updates every Monday at 2AM (UTC). Last updated{" "} + {lastUpdatedHumanReadable} +

)}
diff --git a/listenbrainz/background/export.py b/listenbrainz/background/export.py index deb6fb18d4..02a79443e0 100644 --- a/listenbrainz/background/export.py +++ b/listenbrainz/background/export.py @@ -79,6 +79,7 @@ def export_listens_for_time_range(ts_conn, file_path, user_id: int, start_time: query = """ WITH selected_listens AS ( SELECT l.listened_at + , l.created as inserted_at , l.data , l.recording_msid , COALESCE((data->'additional_info'->>'recording_mbid')::uuid, user_mm.recording_mbid, mm.recording_mbid, other_mm.recording_mbid) AS recording_mbid @@ -97,6 +98,8 @@ def export_listens_for_time_range(ts_conn, file_path, user_id: int, start_time: SELECT jsonb_build_object( 'listened_at' , extract(epoch from listened_at) + , 'inserted_at' + , extract(epoch from inserted_at) , 'track_metadata' , jsonb_set( jsonb_set(data, '{recording_msid}'::text[], to_jsonb(recording_msid::text)), @@ -140,6 +143,7 @@ def export_listens_for_time_range(ts_conn, file_path, user_id: int, start_time: LEFT JOIN LATERAL jsonb_array_elements(artist_data->'artists') WITH ORDINALITY artists(artist, position) ON TRUE GROUP BY sl.listened_at + , sl.inserted_at , sl.recording_msid , sl.data , mbc.recording_mbid diff --git a/listenbrainz/tests/integration/test_export.py b/listenbrainz/tests/integration/test_export.py index a1f026f4ca..cb62e54102 100644 --- a/listenbrainz/tests/integration/test_export.py +++ b/listenbrainz/tests/integration/test_export.py @@ -235,6 +235,10 @@ def test_export(self): self.assertEqual(expected["track_metadata"]["artist_name"], received["track_metadata"]["artist_name"]) self.assertEqual(expected["track_metadata"]["release_name"], received["track_metadata"]["release_name"]) self.assertEqual(expected["listened_at"], received["listened_at"]) + # The test data used in send_listens cannot have an inserted_at prop as that is not a valid listen format + self.assertNotIn("inserted_at", expected) + # However inserted_at should be part of the exported listen data + self.assertIn("inserted_at", received) if received["track_metadata"]["track_name"] == "Sister": self.assertEqual({ "caa_id": self.recording["release_data"]["caa_id"],