Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Audiobookshelf audiobooks & podcast provider #1857

Merged
merged 29 commits into from
Jan 16, 2025

Conversation

fmunkes
Copy link
Contributor

@fmunkes fmunkes commented Jan 12, 2025

Initial support for Audiobookshelf

This PR is an implementation of Audiobookshelf (abs) as music provider.

Features

  • Populates Audiobooks from all libraries accessible by user.
  • Populates Podcasts from all libraries accessible by user.
  • Browse Feature shows the libraries names and podcasts/ books within.
  • Progress reporting both ways (see issues below).
  • API client for mass - I didn't find a suitable python lib, so I implemented what is needed.

Possible future improvements

  • Browse by author, series, narrator etc.
  • Search Feature
  • Edit Feature

Known issues:

  • Progress reporting:
    • abs to mass seems ok
    • mass to abs:
      • on_streamed function of MusicProvider:
        • only called, when next/ previous used in UI (apparently?).
        • not called on pause
        • not called regularly
      • I.e. abs progress is only updated in first case.
    • Which function is called, when "mark as (un)played" is used in the UI?
  • Audiobooks:
    Am I correct, that mass only supports single file based audiobooks? abs may list multiple
    audio files per book, so if this is the case, that's something for the documentation. abs even
    offers an ffmpeg concat via its interface. Code-wise currently only the first file of an audiobook
    is respected.
  • mypy:
    Is currently disabled for this provider. Unfortunately I had troubles with some type hints, due to
    a lack of knowledge on my side. Happy to figure them out.

Personal note: I used Python quite often during the years, however, I'm not a professional developer.
If you think the code is not worth pursuing, don't hesitate to let me know :). I still learned something.

Thanks for the amazing project!

pyproject.toml Outdated Show resolved Hide resolved
@OzGav
Copy link
Contributor

OzGav commented Jan 13, 2025

@fmunkes can you provide some text for the docs? We have the following headings. You don’t need text for all of them. Please see the other providers for ideas of what to add. I could extract most of this from your opening post in the PR but I am conscious of the improvements you may have made since then. Just the text is fine I will do the actual PR for the docs as a number of things need to be done.

FEATURES
CONFIGURATION
KNOWN ISSUES/ NOTES
NOT YET SUPPORTED

@marcelveldt
Copy link
Member

marcelveldt commented Jan 13, 2025

on_streamed function of MusicProvider - only called, when next/ previous used in UI (apparently?).

We're looking into this one, seems to be a bug. Expect to be solved soon.

on_streamed function of MusicProvider - not called on pause

No and that is by design. It will be called when playback completed (full item played or playback stopped) which is enough imo.

on_streamed function of MusicProvider - not called regularly

No and that is by design. Why would we want to report it regularly ?

Which function is called, when "mark as (un)played" is used in the UI?

None, which should be fixed. I will adjust it so it calls on_streamed with 0 seconds or something (I'll chew on it a bit)

Am I correct, that mass only supports single file based audiobooks?

This is still topic of discussion as there seem to be multiple different implementations in the wild but the most common one is a single file per audiobook or podcast-episode with optional chapters.
What we have now is chapter support in a single audiobook/episode. So if you have a file per chapter that will be a challenge

mypy Is currently disabled for this provider.

OK, that will have to be fixed before we can merge but just ask for help and we will help you

@marcelveldt marcelveldt changed the title Audiobookshelf audiobooks & podcast provider Add Audiobookshelf audiobooks & podcast provider Jan 13, 2025
@fmunkes
Copy link
Contributor Author

fmunkes commented Jan 13, 2025

@OzGav documentation:

FEATURES

  • Populates Audiobooks from all libraries accessible by supplied user.
  • Populates Podcasts from all libraries accessible by supplied user.
  • Browse Feature shows the library names and podcasts/ books within.
  • Progress reporting both ways

CONFIGURATION

  • For setup you need
    • the URL of an Audiobookshelf instance
    • username of an Audiobookshelf user
    • password of this user
  • You may optionally disable ssl verification.

KNOWN ISSUES/ NOTES

Audiobookshelf supports multiple files (e.g. one file per chapter) per Audiobook. Music Assistant however expects a single file. It is suggested to merge multiple files into a single file. You can do this in Audiobookshelf by selecting the Audiobook, click edit, then tools and finally "Open Manager". You will see the merge option there. Do not forget to take a backup of your initial files.

NOT YET SUPPORTED

  • Browsing by author
  • Browsing by collection
  • Browsing by series
  • Browsing by playlists

@fmunkes
Copy link
Contributor Author

fmunkes commented Jan 13, 2025

on_streamed function of MusicProvider - only called, when next/ previous used in UI (apparently?).

We're looking into this one, seems to be a bug. Expect to be solved soon.

Thanks - I think my other "on_streamed remarks" become obsolete when this is resolved.

Which function is called, when "mark as (un)played" is used in the UI?

None, which should be fixed. I will adjust it so it calls on_streamed with 0 seconds or something (I'll chew on it a bit)

Thank you!

Am I correct, that mass only supports single file based audiobooks?

This is still topic of discussion as there seem to be multiple different implementations in the wild but the most common one is a single file per audiobook or podcast-episode with optional chapters. What we have now is chapter support in a single audiobook/episode. So if you have a file per chapter that will be a challenge

I think for the time being it is sufficient to just add a comment to the documentation (maybe also to the overall one of 'Audiobooks'?). In the case of Audiobookshelf, merging into one file can be achieved within the UI.

@fmunkes
Copy link
Contributor Author

fmunkes commented Jan 13, 2025

There is a currently a single mypy error left. Everything else was resolved by switching to mashumaro as suggested.
I get:
music_assistant/providers/audiobookshelf/__init__.py:496: error: Incompatible types in assignment (expression has type "ABSAudioBook", variable has type "ABSPodcast") [assignment]
Don't see what's wrong there currently. I'll check again tomorrow.

Copy link
Contributor

@Jc2k Jc2k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RE your mypy error, that's a mypy quirk. Not sure whether its a limitation or a deliberate decision, but you can't use a variable twice for different types. Line 493 makes it think item is a podcast. Then on line 496 item is an audio book. I don't know if there is a better way, but if you had something link async from audiobook in ... it will probably work.

music_assistant/providers/audiobookshelf/abs_client.py Outdated Show resolved Hide resolved
music_assistant/providers/audiobookshelf/icon.svg Outdated Show resolved Hide resolved
@marcelveldt
Copy link
Member

If you rebase your branch, you get a new "on_played" callback:

Scherm­afbeelding 2025-01-15 om 01 19 42

@fmunkes
Copy link
Contributor Author

fmunkes commented Jan 15, 2025

RE: on_played
Thanks a lot! The callback unfortunately doesn't report the correct position for me. I tried the following cases:

  • play (no item in queue)
    • callback is called with id of new stream to be played
  • replace queue
    • callback is called twice, with id of old stream and new stream
  • next / previous
    • same as above
      In all cases, position = 0.

When I try to (un)mark a media item I'm faced with a RuntimeError:
ERROR (MainThread) [music_assistant.webserver] Error handling message: music/mark_played: Value None of type <class 'NoneType'> is invalid for media_item

Am I missing something here?

fmunkes and others added 2 commits January 15, 2025 22:12
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
@fmunkes
Copy link
Contributor Author

fmunkes commented Jan 15, 2025

Comment on audiobooks split accross files:
I learned today, that abs supports sessions, where the abs instance offers you an hls stream of the audiobook. I.e. multiple files would be hidden from mass. This would change the function returning StreamDetails, and add a few other schema definitions plus code complexity. I only played using curl and api calls, nothing in python yet.
I think it is reasonable to have the current PR in a merge-ready state first. I think it does cover many use cases. When that is done, I will see if I can have that feature added as well, possible via a config option of the provider, and would open another PR.

@marcelveldt
Copy link
Member

Comment on audiobooks split accross files: I learned today, that abs supports sessions, where the abs instance offers you an hls stream of the audiobook. I.e. multiple files would be hidden from mass. This would change the function returning StreamDetails, and add a few other schema definitions plus code complexity. I only played using curl and api calls, nothing in python yet. I think it is reasonable to have the current PR in a merge-ready state first. I think it does cover many use cases. When that is done, I will see if I can have that feature added as well, possible via a config option of the provider, and would open another PR.

OK! Keep in mind that you can simply feed the HLS url as url in streamdetails and it will be automagically sorted.

@marcelveldt
Copy link
Member

RE: on_played Thanks a lot! The callback unfortunately doesn't report the correct position for me.

What player did you use to test it ?

@fmunkes
Copy link
Contributor Author

fmunkes commented Jan 16, 2025

OK! Keep in mind that you can simply feed the HLS url as url in streamdetails and it will be automagically sorted.

Ah, excellent, I just had a look at the StreamDetails class. I do have some more time on Saturday/ Sunday, so I could report back by the end of this week on this part. So we could also wait for this, and then continue from there. I guess, I'll return something like this then:

return StreamDetails(
            provider=self.instance_id,
            item_id=audiobook_id,
            audio_format=AudioFormat(
                content_type=ContentType.UNKNOWN,
            ),
            media_type=MediaType.AUDIOBOOK,
            stream_type=StreamType.HLS,
            path=<abs_url>/output.m3u8,
)

i.e. the path is an m3u8 playlist.

@fmunkes
Copy link
Contributor Author

fmunkes commented Jan 16, 2025

What player did you use to test it ?

I used a Chromecast, and tried just now via Airplay, same result. I can also try a player integrated via HomeAssistant, though that will be later today again.

@marcelveldt
Copy link
Member

I guess, I'll return something like this then:

Exactly that should work out of the box! ffmpeg will then care care of stitching it together.

@marcelveldt
Copy link
Member

I used a Chromecast, and tried just now via Airplay, same result. I can also try a player integrated via HomeAssistant, though that will be later today again.

Leave this with me, I will do some more testing and make the reporting more robust.
Also marking played/unplayed us currently broken in the frontend, I need to fix that.

@marcelveldt
Copy link
Member

Let's merge this one as-is now so people can start testing it in the beta cycle.
The multipart handling can then be added later in a small follow-up PR.

Copy link
Member

@marcelveldt marcelveldt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work @fmunkes !

@marcelveldt marcelveldt merged commit fe63706 into music-assistant:dev Jan 16, 2025
1 check passed
@fmunkes fmunkes deleted the provider_audiobookshelf branch January 16, 2025 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants