-
-
Notifications
You must be signed in to change notification settings - Fork 799
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
Big Feature Request: Support for audio books (e.g. m4b) #1419
Comments
Yes most of it is supported already if you use a client with audiobook support (ex: DSub). The only thing I didn't understand is:
Navidrome already support playlists. What do you have in mind? Re: filtering on audiobooks, this will also be possible when we introduce multiple libraries, so you could have a library only for audiobooks, apart from your music library. |
Ok, I made some experiments. First let me say that navidrome is awesome (really, the docker image worked nearly out of the box, even behind an nginx-proxy with Basic Auth - I used https://username:password@myserver.domain/). Since I know audio book stuff pretty well (see https://github.com/sandreas/m4b-tool), here are my thoughts, where I see room for improvement:
So this feature request comes all down to supporting the "media type" with
as well as setting the media type manually for other file types and beeing able to globally filter these and lastly to support embedded chapters. I used the app substreamer with my iPhone and while the raw playback of audio books worked well, the same media type filtering problem applies to the app as well. Chapters did not work either - next/prev button did nothing on audio books. What do you think? |
|
Looks good to me. One problem I can think of is, that Release Type is a bit unclear / unspecific. Although there is a release type "audio book", I'm not sure if ONE data field "release type" is enough to represent two pieces of information:
However, you should definitely consider to set a global filter for "release type" property then to prevent mixing up different types of media in the user interface.
Yes, smart playlists would be awesome. In my opinion smart playlists would be nothing more than "stored filters" and could be used ANYWHERE (including search inputs and music suggestions), which would make them very powerful. One implementation I was very impressed by is the JsonApi.NET one: https://www.jsonapi.net/usage/reading/filtering.html
I understand. You're right, chapter support should go to the upstream project. Although as far as I can tell, chapter support would not be that complex, if you just provide a list of timestamps+titles and "seek" to the according chapter markers timestamp. It is also possible to
I see. Well, maybe there would be a way to workaround this using virtual bookmarks? Bookmarks are normaly user specific, but specified with a "comment", whereas the comment could contain the chapter title prefixed with a special marker to signal the player, that this is not a user defined or editable bookmark, but an auto generated one that represents a chapter... Well, maybe this is not ideal, since:
which would mean, that chapter markers prevent users to generate a bookmark matching a chapter timestamp... I don't know, just an idea :-) I hope you get me right... I'm not trying to tell you how to do your job, I just did some research what it would take to implement this myself, because I really like the project. Unfortunately I doubt that I find the time to contribute code... :-/ Keep up the good work and thank you for the polite and meaningful clarifications. Maybe you could also get some inspiration out of my comments, at least I would hope so. Keep up the good work. |
Ok, I did some further research and took a look at navidrome code. Seems that you are using
So here is what I would like to do:
Here is some code for parsing chapters out of func LoadAudioBookMeta(path string, ffmpegExecutable string, duration time.Duration) (*types.Item, string, error) {
meta := new(types.Item)
absPath, err := filepath.Abs(path)
if err != nil {
println("==> error on getting absolute path of", path)
return nil, "", err
}
// ffmpeg, "-i", f.getAbsolutePath(), "-f", "ffmetadata", "-"
cmdArgs := []string{
"-i", absPath, "-f", "ffmetadata", "-",
}
stdOut, stdErr, err := shell.ExecWithTimeout(ffmpegExecutable, cmdArgs, duration)
if err != nil {
println("==> error on shell exec with timeout while loading meta", err.Error())
return nil, stdOut + stdErr, err
}
var currentChapter *types.Item
timeBase := time.Millisecond
scanner := bufio.NewScanner(strings.NewReader(stdOut))
for scanner.Scan() {
line := scanner.Text()
// ffmpeg could contain newlines in form of "description=here is text \
// here is new line text"
for {
if !strings.HasSuffix(line, "\\") {
break
}
scanner.Scan()
line = strings.TrimSuffix(line, "\\") + "\n" + scanner.Text()
}
if strings.HasPrefix(line, ";") {
continue
}
if strings.ToLower(line) == "[chapter]" {
if currentChapter != nil {
meta.AddChapter(currentChapter)
}
currentChapter = new(types.Item)
continue
}
if currentChapter == nil {
if !strings.Contains(line, "=") {
log.Printf("line %s should be a key-value-pair but does not contain a = separator\n", line)
continue
}
err = meta.SetPair(types.NewKeyValuePairFromString(line, "="))
if err != nil {
log.Printf("line %s results in an empty or unsupported key-value-pair\n", line)
continue
}
} else {
timeBase = handleChapterMetaData(line, timeBase, currentChapter)
}
}
if currentChapter != nil {
meta.AddChapter(currentChapter)
}
return meta, stdOut, nil
}
func handleChapterMetaData(line string, timeBase time.Duration, currentChapter *types.Item) time.Duration {
pair := types.NewKeyValuePairFromString(line, "=")
lowerKey := strings.ToLower(pair.Key)
switch lowerKey {
case "timebase":
timeBasePair := types.NewIndexTotalItemFromString(pair.Value, "/")
if timeBasePair.Index > 0 && timeBasePair.Total > 0 {
timeBase = time.Duration(timeBasePair.Index) * time.Second / time.Duration(timeBasePair.Total) * time.Second
}
case "start":
if startInt, err := strconv.Atoi(pair.Value); err == nil {
currentChapter.Start = time.Duration(startInt) * timeBase
}
case "end":
if endInt, err := strconv.Atoi(pair.Value); err == nil {
currentChapter.End = time.Duration(endInt) * timeBase
}
case "title":
currentChapter.Title = pair.Value
}
return timeBase
} I could try to take care of this, but I see a lot of open pull requests and I'm not sure if this would be a valid solution. My questions:
|
Wow! This is a big reply and I'll try my best to address all of it.
I'm not actually using the FFMETADATA info from ffmpeg, I'm using the stderr output and extracting the values from there. We need some info that is not present in the FFMETADATA output (like channels, bitrate, size and duration) Also, keep in mind that we also support using
I rather store the chapters normalized, and probably in their own table. We can "union" them with bookmarks to implement your suggestion of pseudo-support for chapters By the way, the primary key for bookmarks is the id of the track, so it only supports one bookmark per track, as per Subsonic API requirements:
We can potentially store multiple bookmarks per track (or union them with the chapters as I said above), but I'm not sure how Subsonic clients will behave if they receive more than one bookmark for a given track.... Needs some testing For Navidrome's UI, we can be more "creative", we could potentially add a MenuButton to the player with
We have basically 3 categories open PRs:
To increase the chances of your PR being merged, we should discuss any implementations before hand (as we are doing here), you can ask questions on Discord (easier for me to reply during the day), and you should break the whole implementation in small PRs, easier to review and validate Let me know if you want to implement this and we can discuss further. |
Sry, I'll try to keep this one shorter :-) Let's summarize:
So here is my suggestion. I'm from germany (timezones...) and my time is very limited (family, job), so discord would be a bit difficult, but I would really love to see this happen and I'm passionate for open source. Since I cannot guarantee to submit something good, I'd like to start with a little thing - the chapters storage and extraction. I'll think over this and get back to you soon. My thoughts so far:
Thank you very much for investing the time to work this out. I really hope we can manage this together. |
I don't see that as an issue, the amount of data would be the same if the chapters were embedded in a text field in the media_file. But if you say there's no search based on it, then it may be fine to embed them.
Yeah, we will have to be "creative" if we want this to be supported by Subsonic. Maybe it is not possible at all, let's see
I rather do that at scanner time. Some libraries are stored in cloud storage (like my own library), and I rather avoid access to the files unless I want to play them. And chapters could be returned as a field of the media_file ( |
Ok, I did some further research... DatabaseThe best case in my opinion would be to create a single table storing the chapters referenced to media-id (like in Genres and its struct). Chapters
APIThe only way to support subsonic API constraints I could find was the
Official Example: http://www.subsonic.org/pages/inc/api/examples/playQueue_example_1.xml The XSD specification tells us more details, e.g. that:
So heres is my suggestion:
What do you think? |
Well, it will be a bit different than genres: It will be a one-to-many relationship with I think a good first PR would include the first two items in your task list. Just adding an optional array of chapters in Once we have it in the DB, we can do some experimentation with the bookmarks Subsonic endpoint |
Awesome, I'll try it. |
@deluan So if this can't be delegated or you don't would like to develop it yourself, this issue can be closed (although I would love to see it in the future). |
@sandreas If you have any work up until this point, is it in a branch at all? This is a feature I'm also very interested in, and may be able to take a look at things in the next few months |
@smolenskij Only what I posted here. I did some experiments but discarded them, because I'm still not sure if There are already solutions that work well (audiobookshelf, jellyfin, etc.) and I don't want to reinvent the wheel. However, Currently I am working on a cross platform UI application (Windows, Linux, Mac, Android, iOS, eventually WASM), that comes close to the user interface of my good old iPod Nano with a few tweaks for modern streaming support. It is planned to support multiple data sources / APIs (like navidrome, audiobookshelf, jellyfin etc.) to remove any limitation of self-hosted backends. It is based on AvaloniaUI and LibVLCSharp and currently it looks like this, but this is a HUGE project and maybe I'll never gonna finish it to release state :-) |
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Navidrome team are limited, and so we are asking for your help. |
For audio books I'm now using audiobookshelf + Audiobookshelf App, while for music I still use Navidrome and Substreamer App. Thanks for making this. |
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Describe the solution you'd like
This might be a (too) big feature request... I would love to see support for audio books, since this kind of feature is pretty rare on mature streaming server solutions. The one I'm currently using is https://github.com/advplyr/audiobookshelf, but it would be awesome to have an all in one solution and I really prefer navidrome.
Best case scenario:
m4b
files (including tags)Maybe this request should be divided into multiple ones, but I just would like to ask, if this is beyond the scope of navidrome?
Best,
sandreas
The text was updated successfully, but these errors were encountered: