nostrability icon indicating copy to clipboard operation
nostrability copied to clipboard

WIP: publish music listens as nostr events

Open alltheseas opened this issue 1 year ago • 17 comments

https://njump.me/nevent1qqsv3306jxk3dzq2358yhf7f7lwafr32u7da4kajmxq7qz685qz0q3cpzemhxue69uhhyetvv9ujuurjd9kkzmpwdejhgqg5waehxw309ahx7um5wghx77r5wghxgetkqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyapfnma

alltheseas avatar Aug 29 '24 14:08 alltheseas

It would be cool to integrate/imitate https://github.com/nostr-protocol/nips/pull/1043

staab avatar Aug 29 '24 15:08 staab

we already have music statuses for this...

https://github.com/nostr-protocol/nips/blob/master/38.md#live-statuses

jb55 avatar Aug 31 '24 14:08 jb55

I use this for scrobbling lastfm to nostr:

https://github.com/jb55/lastfmstr

damus displays these as well

jb55 avatar Aug 31 '24 14:08 jb55

I would love it if DVMs could ingest listening history to make recommendations (e.g. building playlists, recommending artists, "discover weekly"), I'm not sure if statuses include enough info for that.

Mostly having the musicbrainz ID and any other relevant data in the scrobble type would be useful.

ebrakke avatar Aug 31 '24 19:08 ebrakke

The current working version of the schema for nostr listens/scrobbles [kind: 2002] is a hybrid of ListenBrainz, Subsonic, and Last.fm conventions. When in doubt it leans towards ListenBrainz conventions, that said, there also needs to be a reasonable tradeoff between precision and utility. Getting down to the specific release and track doesn't really add much unless you are ASCAP.

As a reference, ListenBrainz specifies the following as the minimum required for track as part of track_metadata:

  • artist_name
  • track_name
  • listened_at
  • release_name (optional)

[kind: 2002] opts for album over release as it's a more understandable term. Also drops _name for the key to make it shorter and disambiguation is not required in this context. Will album be optional? Maybe, but it's required for now. Event so far....

  • artist
  • album
  • track
  • created_at

In the case of a [kind: 2002] event, listened_at is the same as created_at. This could change later, but for now the idea is that a nostr event is timestamped based on when the actual event occurred in the real world.

ListenBrainz has a long list of optional tags under additional_info. These are the 3 being targeted.

  • artist_mbids
  • release_group_mbid
  • recording_mbid

Borrowing from NIP-73, [kind: 2002] uses the i tag for MBID identifiers

 "i", "mbid:artist:UUID"
 "i", "mbid:release_group:UUID"
 "i", "mbid:recording:UUID"

Cover Art - Initially the event uses the 'r' or 'image' tags, a roadmap item is Blossom x tags. Ideally a completely distributed last.fm has album art and artist images spread out across the network. For now assume r tags while the concept is being iterated on.

So with that, here is an example of a [kind: 2002] event in it's current state:

{
  "id": "61d01c796fe4f0a4a62db13e26d5923e029a53f352b3cbe74593df9bbe67397e",
  "pubkey": "2ce6f968e7029ac9d347202fdc203ed12e8373ad602fa4f2e4492214a006bf13",
  "created_at": 1723416627,
  "kind": 2002,
  "tags": [
    [
      "album",
      "Muchacho de Lujo"
    ],
    [
      "track",
      "The Quotidian Beasts"
    ],
    [
      "artist",
      "Phosphorescent"
    ],
    [
      "i",
      "mbid:artist:739da2f1-0741-4c60-a6ed-f42d49bf2eb1"
    ],
    [
      "i",
      "mbid:release_group:ba3647fa-e82e-4e39-811d-66307f9f2c42"
    ],
    [
      "i",
      "mbid:release:a1812b30-e3ea-4ea7-b7bd-f2f3bfdc08f0"
    ],
    [
      "r",
      "https://coverartarchive.org/release-group/ba3647fa-e82e-4e39-811d-66307f9f2c42/front"
    ]
  ],
  "content": "Phosphorescent - The Quotidian Beasts",
  "sig": "eb359d95e3a321b8cfb95adafba95a14010941430edd4b3c015b1f0650bfe39a5cf149593ca1c4771a0bc1a983ba0315b91acd705efa243f0bebe2b3e59a5632"
}

Sample screenshot from a publisher application under development.

screenshot_2024-09-01_11 30 17@2x

rossbates avatar Sep 01 '24 16:09 rossbates

@rossbates I was able to whip together a quick node script (using bun) to generate those events from my cmus player

import {$} from 'bun';
import {type EventTemplate, generateSecretKey, finalizeEvent} from 'nostr-tools';

async function getCmusStatus() {
    const output = await $`cmus-remote -C status`.text()
    const lines = output.split('\n')
    const statusLine = lines.find(line => line.startsWith('status '))
    const status = statusLine?.split(' ')[1]
    const position = lines.find(line => line.startsWith('position '))?.split(' ')[1]
    const tags = lines.filter(line => line.startsWith('tag '))
    const tagMap = tags.map(tag => {
        const [_, key, ...value] = tag.split(' ')
        return [key, value.join(' ')]
    }).reduce((acc, [key, value]) => {
        acc[key] = value
        return acc
    }, {} as Record<string, string>)

    return {
        status,
        position,
        tags: tagMap
    }
}

type Status = Awaited<ReturnType<typeof getCmusStatus>>

async function createNostrEvent(status: Status) {
    const event = {
        kind: 2002,
        content: `${status.tags.artist} - ${status.tags.title}`,
        tags: [] as string[][],
        created_at: Math.floor(Date.now() / 1000),
    } satisfies EventTemplate;
    const tags = [
        status.tags.album && ['album', status.tags.album],
        status.tags.artist && ['artist', status.tags.artist],
        status.tags.title && ['track', status.tags.title],
        status.tags.musicbrainz_trackid && ['i', `mbid:recording:${status.tags.musicbrainz_trackid}`]
    ].filter(Boolean) as string[][]
    event.tags = tags
    return event
}

const status = await getCmusStatus()
const event = await createNostrEvent(status)
const sk = generateSecretKey()
const signedEvent = finalizeEvent(event, sk)
console.log(signedEvent)

Resulting output is:

{
  "kind": 2002,
  "content": "Ravi Shankar - Mahaa Mrityunjaya",
  "tags": [
    [
      "album",
      "Chants of India"
    ],
    [
      "artist",
      "Ravi Shankar"
    ],
    [
      "track",
      "Mahaa Mrityunjaya"
    ],
    [
      "i",
      "mbid:recording:3a3f8d42-ec9d-4075-aba1-e7d3da369cf0"
    ]
  ],
  "created_at": 1725279609,
  "pubkey": "593536d68e601bc94aecd3f9d59cb72aaf04a03f69ef961d93dcc733de6ff3da",
  "id": "1bb9182ee95abe08fa1b2421ecbe1dfff9e66ed26026f60caf88293d072b2bf9",
  "sig": "c31a8652f129ee44b7d57d222dfec932605f29e4a87a1241997227b8aebe10aac930cd559f80d9799b0e9cb5051ccbd7ae9be7e87696bf411a15a622a9ee6f1c"
}

ebrakke avatar Sep 02 '24 12:09 ebrakke

One tough part about using blossom for album art is that it's not searchable by the mbid (either album or track) in an easy way, since it's going to be the hash of the actual underlying content.

Would be cool if album art was derived from the already unique UUID that music brainz gives us. It could make it easy for clients to just reach out to their blossom servers to see if the album art exists (and the event could potentially provide a hint as to which blossom servers it knows has that content).

ebrakke avatar Sep 02 '24 12:09 ebrakke

One tough part about using blossom for album art is that it's not searchable by the mbid (either album or track) in an easy way, since it's going to be the hash of the actual underlying content.

Yeah you are correct, using just the sha256 doesn't make it discoverable. You could theoretically extend "kind": 24242 with i tags. BUD-02

A design challenge here is how much you want to put in the event itself vs letting the applications do their lookups and joins. One reason why I am leaning towards using the x tag is this does not leave room for any uncertainty about the image the publisher wants to associate with the event.

The robustness principle states ""be conservative in what you send, be liberal in what you accept". That's roughly how things like ListenBrainz work right now and it makes it way easier on users. You send whatever data you have, and the service will do the heavy lifting of mapping and disambiguation.

On the flip side, if the goal is to completely decentralize a database, the burden needs to be on the publisher to publish the definitive fully encapsulated version of the record. If your references/keys can't stand on their own, something like dcff956e-74a0-4d93-8a3d-edd09d75daf8.jpg could end up being anything the cdn serving it wants it to be.

Validating an event for completeness and accuracy of tags/content is not something I've seen in any other event types yet. The idea could be a total non-starter for the lazy web. We'll see, it's a fun experiment either way.

rossbates avatar Sep 02 '24 15:09 rossbates

Took a look at https://github.com/nostr-protocol/nips/pull/1043 and it would probably integrate nicely with this kind. If we have the 31337 event kind for a music track, then kind 2002 can optionally include a reference to that event.

ebrakke avatar Sep 03 '24 13:09 ebrakke

I'm guessing the ignoring of:

we already have music statuses for this...

https://github.com/nostr-protocol/nips/blob/master/38.md#live-statuses

means this is trying to do something else. Is the goal to have a complete history of every track that was played? then yes that would be different than music statuses which are a replaceable event. statuses are already spammy, but at least they are replaceable, but maybe a non-replaceable type would be fine for a scrobble relay of some kind.

jb55 avatar Sep 03 '24 14:09 jb55

I'm guessing the ignoring of:

we already have music statuses for this... https://github.com/nostr-protocol/nips/blob/master/38.md#live-statuses

means this is trying to do something else. Is the goal to have a complete history of every track that was played? then yes that would be different than music statuses which are a replaceable event. statuses are already spammy, but at least they are replaceable, but maybe a non-replaceable type would be fine for a scrobble relay of some kind.

Yeah, I think this is something different. For instance I would probably publish these things to my own relay so I can maintain my entire listening history (or like you said a scrobble specific relay) And I'd want to use these events to generate things similar to spotify wrapped and recommendations (probably through some sort of DVM)

ebrakke avatar Sep 03 '24 14:09 ebrakke

This is only one person's opinion, but replaceable events don't make sense to me as they carry the implication of state. Sure maybe things are eventually consistent in relays, but that just feels like an application layer concern. I have similar thoughts on delete, but people are free to do whatever they want with their relays and apps and I'll leave it at that. The value I see in nostr is immutable and verifiable records.

For listening history, being able to store everything you have ever listened to is valuable to you as the owner. I don't think they even need to be nostr events, though they could be. Nobody else needs a running list of everything you listen to, except for the data aggregators. And they are tracking much more than your taste in music. Like way more. For peers, knowing you are on song 4/12 of an album, then 5/12, then 6/12.... who cares.

I see a happy medium where scrobbles are expressive. They are a conversation starter. I choose what I want to broadcast, when I want to, for whatever reason. You take the action to broadcast it, it's an intentional act. For the data to be meaningful, it should be accurate and durable, because over time pockets of similar users will find each other. The data is still open to be aggregated, profiled, etc etc.... by any 3rd party, but it's not covert. Users are willingly and knowingly building a public music graph.

rossbates avatar Sep 03 '24 19:09 rossbates

Not really sure if NUDs will actually be a thing, or the best way to go about it, But I created this https://github.com/ebrakke/nostr-scrobbler

I put my cmus project in there (will add other projects that interop with this event kind), but would be cool to just build things using this spec and see what comes of it / how it works. If it cool, then great! It could be a NIP. If no one uses it, also cool! Doesn't need to be part of the broader NIP ecosystem, but people could still build onit.

ebrakke avatar Sep 06 '24 12:09 ebrakke

@ebrakke this is super cool, love it!! Also agree with your approach, let's just have fun and see what happens.

Been thinking about how realistic it is to expect people to use the mbid scheme. It could be optional of course, but do you personally use them for your library? Are your meticulous about embedding them in tags and checking for accuracy?

rossbates avatar Sep 06 '24 23:09 rossbates

Yeah I use music brainz picard on all my music files before they make it into my library, so all of my stuff has the mbid on it. The i tag could probably just be more general and clients could identify it by it's scheme. Like the spotify is spotify:track:{id}, I'm sure other streaming services have the same.

ebrakke avatar Sep 07 '24 03:09 ebrakke

Just published a few things to the nostr-scrobbler project

  1. A custom relay using fiatjaf's khatru. It serves up a page of scrobble stats and also act as a relay. I'm thinking i'll only have it accept 2002 events for now, and then can open it up to more if new events are needed.
  2. lastfm-to-nostr - Basically like lastfmstr that jb55 posted, but it'll convert the scrobble into a kind 2002 event and publish to a relay (preserving the scroblled at time as the created_at for the event)
  3. scrobbler-dvm - Mostly just a POC that I can load in the events into python and start doing fun pandas stuff on it. I feel like this project is where a lot of the recommendation power will come in, but also just being able to have statistics about your own listening is pretty fun. Also it'd be cool to hook up some AI to your listening history as well, wonder if there's any models trained on music...

I'm planning on hosting a relay for these scrobble events, will be fun to see what the nostr music network graph starts to look like.

ebrakke avatar Sep 07 '24 17:09 ebrakke

See https://njump.me/note12wxrhjy3wm3nn363kq0nfk34lrcw5e5md0v5q6wfhpmku2xs22sqnx3yln fanfares pod plans by arkinox

alltheseas avatar Sep 08 '24 22:09 alltheseas