[sync] Merge remote-tracking branch 'origin/master' into bulma-1.0

This commit is contained in:
Alain Nussbaumer 2025-02-04 22:08:54 +01:00
commit e9ed220853
21 changed files with 1071 additions and 564 deletions

249
ChangeLog
View File

@ -1,7 +1,21 @@
ChangeLog for OwnTone # Changelog
---------------------
## Version 28.11 - 2025-01-26
- fix: retrieval of artwork from online sources
- fix: mpd speaker selection
- fix: autoconf warnings
- fix: Apple Music/iTunes not working on Airplay host
- web UI: Now Playing does not stop play progress updates when pausing
- web UI: add ability to access the server externally
- new: internet radio "Streamurl" options
- new: support libevent as WS server instead of libwebsockets
- new: template for VSCode
- new: mpd updates, version 0.23.0, e.g. getvol, readpicture, albumart
- new: API for setting skip_count and play_count directly
## Version 28.10 - 2024-09-12
version 28.10
- fix: playlist scanner ignoring lines starting with non-ascii chars - fix: playlist scanner ignoring lines starting with non-ascii chars
- fix: last seconds of a track sometimes being skipped - fix: last seconds of a track sometimes being skipped
- fix: Apple Music password-based auth - fix: Apple Music password-based auth
@ -21,11 +35,12 @@ version 28.10
- config: deprecate "cache_path", replaced by "cache_dir" - config: deprecate "cache_path", replaced by "cache_dir"
- dependency: libxml2 instead of mxml - dependency: libxml2 instead of mxml
version 28.9 ## Version 28.9 - 2024-01-18
- web UI improvements: - web UI improvements:
display lyrics metadata - display lyrics metadata
toggle Spotify on/off in some views - toggle Spotify on/off in some views
many minor improvements - many minor improvements
- use compressed ALAC for Airplay for bandwidth + fixes esp32 issue - use compressed ALAC for Airplay for bandwidth + fixes esp32 issue
- don't merge Spotify albums with local albums - don't merge Spotify albums with local albums
- handle playlist with Unicode BOM - handle playlist with Unicode BOM
@ -36,7 +51,8 @@ version 28.9
- fix FreeBSD possible crash - fix FreeBSD possible crash
- fix crash when keys of incorrect length are used for legacy pairing - fix crash when keys of incorrect length are used for legacy pairing
version 28.8 ## Version 28.8
- fix MacOS bind error: "Protocol wrong type for socket" - fix MacOS bind error: "Protocol wrong type for socket"
- fix BSD build error (no SYS_gettid) - fix BSD build error (no SYS_gettid)
- fix ALAC missing end tag causing ffmpeg decoder warnings - fix ALAC missing end tag causing ffmpeg decoder warnings
@ -44,15 +60,16 @@ version 28.8
- fix duplicates if file within library is replaced - fix duplicates if file within library is replaced
- fix fatal error due to mutex being zeroed - fix fatal error due to mutex being zeroed
version 28.7 ## Version 28.7
- fix compability with ffmpeg 6 - fix compability with ffmpeg 6
- web UI improvements: - web UI improvements:
easier volume sliders - easier volume sliders
incorrect display of genre - incorrect display of genre
Chinese translation - Chinese translation
fix removing RSS podcasts - fix removing RSS podcasts
sort by rating for composer, genre and artist tracks - sort by rating for composer, genre and artist tracks
(and much more) - (and much more)
- changes to artwork search orders (easier static artwork for pipes) - changes to artwork search orders (easier static artwork for pipes)
- major refactor of the http server improving mp3 streaming - major refactor of the http server improving mp3 streaming
- support for m3u8 playlist files - support for m3u8 playlist files
@ -60,7 +77,8 @@ version 28.7
- fix issue with device name capitalization (TuneAero issue) - fix issue with device name capitalization (TuneAero issue)
- drop support for libevent < 2.1.4 - drop support for libevent < 2.1.4
version 28.6 ## Version 28.6
- German translation of web UI - German translation of web UI
- web UI: fix error messages not displaying - web UI: fix error messages not displaying
- fix low resolution Spotify artwork - fix low resolution Spotify artwork
@ -69,7 +87,8 @@ version 28.6
- support password authentication for Airplay 2 - support password authentication for Airplay 2
- support for user config ffmpeg audio filters - support for user config ffmpeg audio filters
version 28.5 ## Version 28.5
- French translation of web UI - French translation of web UI
- improved web UI loading of images - improved web UI loading of images
- add support for Airplay 2 password based auth - add support for Airplay 2 password based auth
@ -79,7 +98,8 @@ version 28.5
- fix for Remote - play item from 'up next' when stopped - fix for Remote - play item from 'up next' when stopped
- use configured bind_address to set mdns network interface - use configured bind_address to set mdns network interface
version 28.4 ## Version 28.4
- fix broken Spotify after libspotify sunset - fix broken Spotify after libspotify sunset
- remove antlr3 dependency, use bison/flex instead - remove antlr3 dependency, use bison/flex instead
- improve search by supporting diacritics and Unicode case folding - improve search by supporting diacritics and Unicode case folding
@ -88,25 +108,26 @@ version 28.4
- smart playlists fixups and new "this week" param - smart playlists fixups and new "this week" param
- fix 'add next' when in queue shuffle mode - fix 'add next' when in queue shuffle mode
- web UI improvements: - web UI improvements:
migration to Vue 3 and Vite - migration to Vue 3 and Vite
honor "radio_playlists" config setting - honor "radio_playlists" config setting
display of search results for composers and playlists - display of search results for composers and playlists
add album / track count to genre and composer pages - add album / track count to genre and composer pages
fix incorrect sorting of album/artist searches - fix incorrect sorting of album/artist searches
minor UI fixes - minor UI fixes
- fix for Spotify config option album_override - fix for Spotify config option album_override
- improved Spotify scan performance - improved Spotify scan performance
- generic browse endpoints for the json api - generic browse endpoints for the json api
- fix slow shutdown with some libwebsocket versions - fix slow shutdown with some libwebsocket versions
version 28.3 ## Version 28.3
- web UI improvements, e.g.: - web UI improvements, e.g.:
composer views - composer views
partial scan (e.g. only update RSS feeds) - partial scan (e.g. only update RSS feeds)
fix http stream button not clickable in mobile view - fix http stream button not clickable in mobile view
fix Spotify playlists not showing - fix Spotify playlists not showing
handling of not playable Spotify tracks - handling of not playable Spotify tracks
handling of podcast play counts - handling of podcast play counts
- support for Spotify podcasts - support for Spotify podcasts
- updates for ffmpeg 5 - updates for ffmpeg 5
- better Spotify logout - better Spotify logout
@ -117,12 +138,13 @@ version 28.3
- fix rare Airplay pairing error - fix rare Airplay pairing error
- many minor error handling fixes - many minor error handling fixes
version 28.2 ## Version 28.2
- add Spotify integration that doesn't depend on libspotify - add Spotify integration that doesn't depend on libspotify
- partial support for AirPlay events (Homepod buttons) - partial support for AirPlay events (Homepod buttons)
- web UI upgraded, now 1.1.0: - web UI upgraded, now 1.1.0:
show "comment" field in track details - show "comment" field in track details
drop double login to Spotify when not using libspotify - drop double login to Spotify when not using libspotify
- easier install by letting 'make install' add user and service files - easier install by letting 'make install' add user and service files
- preserve existing conf file when running 'make install' - preserve existing conf file when running 'make install'
- support for "comment" field when making smart playlists - support for "comment" field when making smart playlists
@ -133,10 +155,12 @@ version 28.2
- fix for CVE-2021-38383 - fix for CVE-2021-38383
- fix some minor time-of-check time-of-use bugs - fix some minor time-of-check time-of-use bugs
version 28.1 ## Version 28.1
- fix incompability in 28.0 with Debian Buster's libwebsockets 2.0 - fix incompability in 28.0 with Debian Buster's libwebsockets 2.0
version 28.0 ## Version 28.0
- rename forked-daapd to OwnTone + new logo - rename forked-daapd to OwnTone + new logo
- fix web UI slow updates due to websockets 3.x changes - fix web UI slow updates due to websockets 3.x changes
- support for ALAC sort tags - support for ALAC sort tags
@ -148,22 +172,24 @@ version 28.0
- refactor how the server binds to sockets (use dual stack ipv4/6) - refactor how the server binds to sockets (use dual stack ipv4/6)
- configurable interface/address binding - configurable interface/address binding
version 27.4 ## Version 27.4
- fix web server path traversal vulnerability - fix web server path traversal vulnerability
version 27.3 ## Version 27.3
- support for AirPlay 2 speakers, incl. compressed ALAC - support for AirPlay 2 speakers, incl. compressed ALAC
- web UI upgraded, now v0.8.5: - web UI upgraded, now v0.8.5:
new design/layout - new design/layout
optimize "Recently added" - optimize "Recently added"
Spotify search dialogue improvements - Spotify search dialogue improvements
drop separate admin web page, now integrated in main web - drop separate admin web page, now integrated in main web
podcast deletion - podcast deletion
make Radio a top level item - make Radio a top level item
show release dates - show release dates
new sorting options - new sorting options
prevent browser caching of playlists - prevent browser caching of playlists
additional settings - additional settings
- improved Chromecast streaming (retransmisson, adaptive etc.) - improved Chromecast streaming (retransmisson, adaptive etc.)
- JSON api support for updating metadata of queue items - JSON api support for updating metadata of queue items
- JSON api new fields, e.g. time_added, time_played and seek - JSON api new fields, e.g. time_added, time_played and seek
@ -180,22 +206,23 @@ version 27.3
- support shairport-sync metadata pipe flush event - support shairport-sync metadata pipe flush event
- misc logging fixup - misc logging fixup
version 27.2 ## Version 27.2
- web UI upgraded to v0.7.2: - web UI upgraded to v0.7.2:
show cover artwork in album pages and lazy loading of artwork - show cover artwork in album pages and lazy loading of artwork
show playlist folders - show playlist folders
use sass/scss for css files - use sass/scss for css files
add "Radio" tab to the music section - add "Radio" tab to the music section
add settings for artwork sources - add settings for artwork sources
add pop up dialog for Remote pairing requests - add pop up dialog for Remote pairing requests
support adding/removing podcast subscriptions - support adding/removing podcast subscriptions
support marking all new podcast episodes/all episodes as played - support marking all new podcast episodes/all episodes as played
support searching by smart pl queries - support searching by smart pl queries
skip buttons for audiobooks and podcasts - skip buttons for audiobooks and podcasts
show localized times/dates - show localized times/dates
generate colored placeholder image if cover artwork is missing - generate colored placeholder image if cover artwork is missing
show "cast" icon for Chromecast outputs - show "cast" icon for Chromecast outputs
styling changes of the navbars and moving the volume controls - styling changes of the navbars and moving the volume controls
- new speaker selection logic (persist user choice even after failure) - new speaker selection logic (persist user choice even after failure)
- speaker autoselect no longer enabled by default - speaker autoselect no longer enabled by default
- removed old admin page, not necessary any more - removed old admin page, not necessary any more
@ -216,7 +243,8 @@ version 27.2
- drop libspotify for artwork, doesn't work any more - drop libspotify for artwork, doesn't work any more
- documentation improvements - documentation improvements
version 27.1 ## Version 27.1
- web UI upgraded to v0.6.0: settings page, display more Spotify data - web UI upgraded to v0.6.0: settings page, display more Spotify data
- support for volumeup, volumedown and mutetoggle DACP commands - support for volumeup, volumedown and mutetoggle DACP commands
- support for multiple ALSA devices - support for multiple ALSA devices
@ -226,7 +254,8 @@ version 27.1
- fix for incorrect update of time_added metadata - fix for incorrect update of time_added metadata
- fix some small memleaks and missing cleanup - fix some small memleaks and missing cleanup
version 27.0 ## Version 27.0
- no fixed resampling to 44100/16, play source quality if possible - no fixed resampling to 44100/16, play source quality if possible
- Chromecast: quick start, better quality (48000/16 Opus encoded) - Chromecast: quick start, better quality (48000/16 Opus encoded)
- performance enhancements: Remote and iTunes will load quicker - performance enhancements: Remote and iTunes will load quicker
@ -245,7 +274,8 @@ version 27.0
- support for some http seeking - support for some http seeking
- fix for macOS Catalinas Apple Music - fix for macOS Catalinas Apple Music
version 26.5 ## Version 26.5
- json api/web ui: file view - json api/web ui: file view
- web ui: artwork support - web ui: artwork support
- web ui: "Add next" and genre tab - web ui: "Add next" and genre tab
@ -260,14 +290,16 @@ version 26.5
- mpd version 0.20 support + support for "listfiles" command - mpd version 0.20 support + support for "listfiles" command
- fix double http auth decline issue - fix double http auth decline issue
version 26.4: ## Version 26.4
- automatic rating - automatic rating
- fix issue in 26.3 causing invalid time_skipped values in the db - fix issue in 26.3 causing invalid time_skipped values in the db
- improved fallback to ipv4 if ipv6 fails - improved fallback to ipv4 if ipv6 fails
- fix issue returning too many queue items to clients - fix issue returning too many queue items to clients
- fix missing prompt for library password - fix missing prompt for library password
version 26.3: ## Version 26.3
- fix AirPlay 2 devices (e.g. Sonos Beam and Airport Express) - fix AirPlay 2 devices (e.g. Sonos Beam and Airport Express)
- fix mdns problems with ATV4 and ipv6 - fix mdns problems with ATV4 and ipv6
- fix possible segfault if null user-agent - fix possible segfault if null user-agent
@ -276,10 +308,12 @@ version 26.3:
- fix for crashes when client provides no User-Agent - fix for crashes when client provides no User-Agent
- logging improvements - logging improvements
version 26.2: ## Version 26.2
- fix for db indexes not being used on fresh installs - fix for db indexes not being used on fresh installs
version 26.1: ## Version 26.1
- player web interface - player web interface
- support for Airplay speaker control commands - support for Airplay speaker control commands
- add non-library items (e.g. radio stations) to the queue - add non-library items (e.g. radio stations) to the queue
@ -290,7 +324,8 @@ version 26.1:
- fix ffmpeg segfault when jpeg encoding - fix ffmpeg segfault when jpeg encoding
- performance improvements + misc - performance improvements + misc
version 26.0: ## Version 26.0
- added web interface - added web interface
- added JSON API - added JSON API
- new mpd commands (e.g. sticker, urlhandlers, playlistfind) - new mpd commands (e.g. sticker, urlhandlers, playlistfind)
@ -309,7 +344,8 @@ version 26.0:
- ffmpeg/transcoding refactored for new ffmpeg API - ffmpeg/transcoding refactored for new ffmpeg API
- and more... - and more...
version 25.0: ## Version 25.0
- improved playback resilience - improved playback resilience
- substitute packet skipping (producing audio "clicks") with start/stop - substitute packet skipping (producing audio "clicks") with start/stop
- support for MacOSX with macports and Bonjour mDNS - support for MacOSX with macports and Bonjour mDNS
@ -330,7 +366,8 @@ version 25.0:
- performance improvements - performance improvements
- and other fixing up... - and other fixing up...
version 24.2: ## Version 24.2
- Pulseaudio support (can be used for Bluetooth speakers) - Pulseaudio support (can be used for Bluetooth speakers)
- new pipe/"fifo" audio output - new pipe/"fifo" audio output
- fix misc Chromecast audio bugs - fix misc Chromecast audio bugs
@ -345,11 +382,13 @@ version 24.2:
- fix possible segfault on http timeouts - fix possible segfault on http timeouts
- fix possible segfault when adding items during playback - fix possible segfault when adding items during playback
version 24.1: ## Version 24.1
- support for Monkey's audio - support for Monkey's audio
- fix build problems on some platforms (e.g. OpenWrt) - fix build problems on some platforms (e.g. OpenWrt)
version 24.0: ## Version 24.0
- support for Chromecast audio - support for Chromecast audio
- support more idv3 tags (eg. date released) - support more idv3 tags (eg. date released)
- support more DAAP tags (eg. datereleased, hasbeenplayed) - support more DAAP tags (eg. datereleased, hasbeenplayed)
@ -369,7 +408,8 @@ version 24.0:
- support for old ffmpeg dropped - support for old ffmpeg dropped
- misc minor bugfixing - misc minor bugfixing
version 23.4: ## Version 23.4
- fix freeze problem on network stream disconnects - fix freeze problem on network stream disconnects
- support for mp3 streaming - support for mp3 streaming
- better ipv6 handling - better ipv6 handling
@ -379,7 +419,8 @@ version 23.4:
- libavresample/libswresample dependency changed to libavfilter - libavresample/libswresample dependency changed to libavfilter
- improved pairinghelper.sh script - improved pairinghelper.sh script
version 23.3: ## Version 23.3
- fix issue where volume gets set to -1 on startup of raop devices - fix issue where volume gets set to -1 on startup of raop devices
- plug various minor memleaks - plug various minor memleaks
- audiobook improvements, eg resuming playback from saved position - audiobook improvements, eg resuming playback from saved position
@ -389,29 +430,35 @@ version 23.3:
- drop legacy ffmpeg stuff - drop legacy ffmpeg stuff
- drop legacy flac, musepack and wma scanner - drop legacy flac, musepack and wma scanner
version 23.2: ## Version 23.2
- fix db lock, m3u and Windows Phone bugs - fix db lock, m3u and Windows Phone bugs
- improvements for Spotify and mpd - improvements for Spotify and mpd
- fixing bugs as always - fixing bugs as always
- sorting of genres and composers - sorting of genres and composers
version 23.1: ## Version 23.1
- support for more mpd commands - support for more mpd commands
version 23.0: ## Version 23.0
- add support for the mpd protocol - add support for the mpd protocol
- add support for smart playlists - add support for smart playlists
- playlist and internet stream overhaul - playlist and internet stream overhaul
version 22.2: ## Version 22.2
- fix for iTunes 12.1 - fix for iTunes 12.1
- fix misc bugs - fix misc bugs
version 22.1: ## Version 22.1
- artwork cache - artwork cache
- some Spotify fixing up - some Spotify fixing up
version 22.0: ## Version 22.0
- queue handling improvements - queue handling improvements
- added DAAP cache, good for low-power devices like the RPi - added DAAP cache, good for low-power devices like the RPi
- support for LastFM scrobbling - support for LastFM scrobbling
@ -424,7 +471,8 @@ version 22.0:
- fix segfault on invalid utf8 while sorting - fix segfault on invalid utf8 while sorting
- fix misc bugs - fix misc bugs
version 21.0: ## Version 21.0
- filescanner performance enhancements (db transactions) - filescanner performance enhancements (db transactions)
- support for queue editing - support for queue editing
- support for showing history - support for showing history
@ -443,7 +491,8 @@ version 21.0:
- fix bug in m3u scanner - fix bug in m3u scanner
- ICY metadata fixes - ICY metadata fixes
version 20.0: ## Version 20.0
- includes patch against timeouts - includes patch against timeouts
- configurable artwork file names - configurable artwork file names
- support for Remote 3 and 4 - support for Remote 3 and 4
@ -479,29 +528,34 @@ version 20.0:
- fixes for video playback - fixes for video playback
- other fixes: non apple players, ffmpeg/libav updates... - other fixes: non apple players, ffmpeg/libav updates...
version 0.19: ## Version 0.19
- more libav 0.7 updates. - more libav 0.7 updates.
- database speedups. - database speedups.
- fix for iTunes 30-minute timeout. - fix for iTunes 30-minute timeout.
- fixes, big and small. - fixes, big and small.
version 0.18: ## Version 0.18
- add config knob for ALSA mixer channel name. - add config knob for ALSA mixer channel name.
- do not elevate privileges for reopening the log file; log file - do not elevate privileges for reopening the log file; log file
will now be owned by the user forked-daapd runs as. will now be owned by the user forked-daapd runs as.
- fixes, big and small. - fixes, big and small.
version 0.17: ## Version 0.17
- support for libav 0.7 - support for libav 0.7
- fixes, big and small. - fixes, big and small.
version 0.16: ## Version 0.16
- fix issue with non-UTF-8 metadata while scanning. - fix issue with non-UTF-8 metadata while scanning.
- use proper file size in HTTP streaming code. - use proper file size in HTTP streaming code.
- fix DAAP songlist bug with sort tags. - fix DAAP songlist bug with sort tags.
- small code fixes. - small code fixes.
version 0.15: ## Version 0.15
- add support for sending metadata to AppleTV during AirTunes streaming. - add support for sending metadata to AppleTV during AirTunes streaming.
- support DOS-encoded Remote pairing files. - support DOS-encoded Remote pairing files.
- rework album_artist_sort handling. - rework album_artist_sort handling.
@ -511,7 +565,8 @@ version 0.15:
- artwork can handle and send out both PNG and JPEG. - artwork can handle and send out both PNG and JPEG.
- fixes, big and small. - fixes, big and small.
version 0.14: ## Version 0.14
- sort headers/tags handling improvements. - sort headers/tags handling improvements.
- better handling of tags for TV shows. - better handling of tags for TV shows.
- better handling of DRM-afflicted files. - better handling of DRM-afflicted files.
@ -519,7 +574,8 @@ version 0.14:
- fix scanning of URL files. - fix scanning of URL files.
- fixes, big and small. - fixes, big and small.
version 0.13: ## Version 0.13
- add Remote v2 support; Remote v1 is not supported anymore. - add Remote v2 support; Remote v1 is not supported anymore.
- add per-speaker volume support. - add per-speaker volume support.
- implement RAOP retransmission. - implement RAOP retransmission.
@ -532,7 +588,8 @@ version 0.13:
- FFmpeg 0.6 support. - FFmpeg 0.6 support.
- fixes, big and small. - fixes, big and small.
version 0.12: ## Version 0.12
- add AirTunes v2 streaming. - add AirTunes v2 streaming.
- add Remote support. - add Remote support.
- add gzipped replies. - add gzipped replies.
@ -540,7 +597,8 @@ version 0.12:
- check for UTF-8 correctness of metadata. - check for UTF-8 correctness of metadata.
- fixes, big and small. - fixes, big and small.
version 0.11: ## Version 0.11
- support iTunes 9. - support iTunes 9.
- add iTunes XML playlist scanner. - add iTunes XML playlist scanner.
- add support for TV shows. - add support for TV shows.
@ -552,5 +610,6 @@ version 0.11:
- preliminary support for Remote (pairing, browsing). - preliminary support for Remote (pairing, browsing).
- fixes, big and small. - fixes, big and small.
version 0.10: ## Version 0.10
- initial release. - initial release.

View File

@ -1,7 +1,7 @@
dnl Process this file with autoconf to produce a configure script. dnl Process this file with autoconf to produce a configure script.
AC_PREREQ([2.60]) AC_PREREQ([2.60])
AC_INIT([owntone], [28.10]) AC_INIT([owntone], [28.11])
AC_CONFIG_SRCDIR([config.h.in]) AC_CONFIG_SRCDIR([config.h.in])
AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_MACRO_DIR([m4])

1
docs/changelog.md Normal file
View File

@ -0,0 +1 @@
--8<-- "ChangeLog"

File diff suppressed because one or more lines are too long

View File

@ -46,18 +46,27 @@ theme:
# - navigation.indexes # - navigation.indexes
- navigation.top - navigation.top
palette: palette:
- scheme: default # Palette toggle for automatic mode
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: white primary: white
accent: teal accent: teal
toggle: toggle:
icon: material/toggle-switch icon: material/brightness-7
name: Switch to dark mode name: Switch to dark mode
- scheme: slate # Palette toggle for dark mode
primary: blue grey - media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
accent: teal accent: teal
toggle: toggle:
icon: material/toggle-switch-off-outline icon: material/brightness-4
name: Switch to light mode name: Switch to system preference
font: font:
text: Roboto text: Roboto
code: Roboto Mono code: Roboto Mono
@ -112,6 +121,9 @@ markdown_extensions:
repo: mkdocs-material repo: mkdocs-material
- pymdownx.mark - pymdownx.mark
- pymdownx.smartsymbols - pymdownx.smartsymbols
- pymdownx.snippets:
base_path: [!relative $config_dir]
check_paths: true
- pymdownx.superfences: - pymdownx.superfences:
custom_fences: custom_fences:
- name: mermaid - name: mermaid
@ -159,4 +171,5 @@ nav:
- Remote Access: advanced/remote-access.md - Remote Access: advanced/remote-access.md
- Multiple Instances: advanced/multiple-instances.md - Multiple Instances: advanced/multiple-instances.md
- Development: development.md - Development: development.md
- Changelog: changelog.md
- JSON API: json-api.md - JSON API: json-api.md

View File

@ -20,7 +20,7 @@ Debug domains; available domains are: \fIconfig\fP, \fIdaap\fP,
\fIdb\fP, \fIhttpd\fP, \fImain\fP, \fImdns\fP, \fImisc\fP, \fIdb\fP, \fIhttpd\fP, \fImain\fP, \fImdns\fP, \fImisc\fP,
\fIrsp\fP, \fIscan\fP, \fIxcode\fP, \fIevent\fP, \fIhttp\fP, \fIremote\fP, \fIrsp\fP, \fIscan\fP, \fIxcode\fP, \fIevent\fP, \fIhttp\fP, \fIremote\fP,
\fIdacp\fP, \fIffmpeg\fP, \fIartwork\fP, \fIplayer\fP, \fIraop\fP, \fIdacp\fP, \fIffmpeg\fP, \fIartwork\fP, \fIplayer\fP, \fIraop\fP,
\fIlaudio\fP, \fIdmap\fP, \fIfdbperf\fP, \fIspotify\fP, \fIlastfm\fP, \fIlaudio\fP, \fIdmap\fP, \fIfdbperf\fP, \fIspotify\fP, \fIscrobble\fP,
\fIcache\fP, \fImpd\fP, \fIstream\fP, \fIcast\fP, \fIfifo\fP, \fIlib\fP, \fIcache\fP, \fImpd\fP, \fIstream\fP, \fIcast\fP, \fIfifo\fP, \fIlib\fP,
\fIweb\fP, \fIairplay\fP. \fIweb\fP, \fIairplay\fP.
.TP .TP

View File

@ -126,6 +126,7 @@ owntone_SOURCES = main.c \
evthr.c evthr.h \ evthr.c evthr.h \
$(SPOTIFY_SRC) \ $(SPOTIFY_SRC) \
$(LASTFM_SRC) \ $(LASTFM_SRC) \
listenbrainz.c listenbrainz.h \
$(MPD_SRC) \ $(MPD_SRC) \
listener.c listener.h \ listener.c listener.h \
commands.c commands.h \ commands.c commands.h \

View File

@ -72,6 +72,7 @@ enum query_type {
#define DB_ADMIN_START_TIME "start_time" #define DB_ADMIN_START_TIME "start_time"
#define DB_ADMIN_LASTFM_SESSION_KEY "lastfm_sk" #define DB_ADMIN_LASTFM_SESSION_KEY "lastfm_sk"
#define DB_ADMIN_SPOTIFY_REFRESH_TOKEN "spotify_refresh_token" #define DB_ADMIN_SPOTIFY_REFRESH_TOKEN "spotify_refresh_token"
#define DB_ADMIN_LISTENBRAINZ_TOKEN "listenbrainz_token"
/* Max value for media_file_info->rating (valid range is from 0 to 100) */ /* Max value for media_file_info->rating (valid range is from 0 to 100) */
#define DB_FILES_RATING_MAX 100 #define DB_FILES_RATING_MAX 100

View File

@ -21,7 +21,7 @@ struct http_client_ctx
*/ */
const char *url; const char *url;
struct keyval *output_headers; struct keyval *output_headers;
char *output_body; const char *output_body;
/* A keyval/evbuf to store response headers and body. /* A keyval/evbuf to store response headers and body.
* Can be set to NULL to ignore that part of the response. * Can be set to NULL to ignore that part of the response.
@ -37,10 +37,6 @@ struct http_client_ctx
/* HTTP Response code */ /* HTTP Response code */
int response_code; int response_code;
/* Private */
int ret;
void *evbase;
}; };
struct http_icy_metadata struct http_icy_metadata

View File

@ -54,6 +54,7 @@
#ifdef LASTFM #ifdef LASTFM
# include "lastfm.h" # include "lastfm.h"
#endif #endif
#include "listenbrainz.h"
#ifdef HAVE_LIBWEBSOCKETS #ifdef HAVE_LIBWEBSOCKETS
# include "websocket.h" # include "websocket.h"
#endif #endif
@ -162,16 +163,17 @@ playcount_inc_cb(void *arg)
db_file_inc_playcount(*id); db_file_inc_playcount(*id);
} }
#ifdef LASTFM
/* Callback from the worker thread (async operation as it may block) */ /* Callback from the worker thread (async operation as it may block) */
static void static void
scrobble_cb(void *arg) scrobble_cb(void *arg)
{ {
int *id = arg; int *id = arg;
#ifdef LASTFM
lastfm_scrobble(*id); lastfm_scrobble(*id);
}
#endif #endif
listenbrainz_scrobble(*id);
}
static const char * static const char *
content_type_from_ext(const char *ext) content_type_from_ext(const char *ext)
@ -672,9 +674,7 @@ stream_end_register(struct stream_ctx *st)
{ {
st->no_register_playback = true; st->no_register_playback = true;
worker_execute(playcount_inc_cb, &st->id, sizeof(int), 0); worker_execute(playcount_inc_cb, &st->id, sizeof(int), 0);
#ifdef LASTFM
worker_execute(scrobble_cb, &st->id, sizeof(int), 1); worker_execute(scrobble_cb, &st->id, sizeof(int), 1);
#endif
} }
} }

View File

@ -47,6 +47,7 @@
# include "lastfm.h" # include "lastfm.h"
#endif #endif
#include "library.h" #include "library.h"
#include "listenbrainz.h"
#include "logger.h" #include "logger.h"
#include "misc.h" #include "misc.h"
#include "misc_json.h" #include "misc_json.h"
@ -1447,6 +1448,77 @@ jsonapi_reply_lastfm_logout(struct httpd_request *hreq)
return HTTP_NOCONTENT; return HTTP_NOCONTENT;
} }
static int
jsonapi_reply_listenbrainz(struct httpd_request *hreq)
{
struct listenbrainz_status status;
json_object *jreply;
listenbrainz_status_get(&status);
CHECK_NULL(L_WEB, jreply = json_object_new_object());
json_object_object_add(jreply, "enabled", json_object_new_boolean(!status.disabled));
json_object_object_add(jreply, "token_valid", json_object_new_boolean(status.token_valid));
if (status.user_name)
json_object_object_add(jreply, "user_name", json_object_new_string(status.user_name));
if (status.message)
json_object_object_add(jreply, "message", json_object_new_string(status.message));
CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply)));
jparse_free(jreply);
listenbrainz_status_free(&status, true);
return HTTP_OK;
}
static int
jsonapi_reply_listenbrainz_token_add(struct httpd_request *hreq)
{
json_object *request;
const char *token;
int ret;
request = jparse_obj_from_evbuffer(hreq->in_body);
if (!request)
{
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n");
return HTTP_BADREQUEST;
}
token = jparse_str_from_obj(request, "token");
ret = listenbrainz_token_set(token);
jparse_free(request);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "Failed to set ListenBrainz token\n");
return HTTP_INTERNAL;
}
return HTTP_NOCONTENT;
}
static int
jsonapi_reply_listenbrainz_token_delete(struct httpd_request *hreq)
{
int ret;
ret = listenbrainz_token_delete();
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "Failed to delete ListenBrainz token\n");
return HTTP_INTERNAL;
}
return HTTP_NOCONTENT;
}
/* /*
* Kicks off pairing of a daap/dacp client * Kicks off pairing of a daap/dacp client
* *
@ -4658,6 +4730,10 @@ static struct httpd_uri_map adm_handlers[] =
{ HTTPD_METHOD_GET, "^/api/search$", jsonapi_reply_search }, { HTTPD_METHOD_GET, "^/api/search$", jsonapi_reply_search },
{ HTTPD_METHOD_GET, "^/api/listenbrainz$", jsonapi_reply_listenbrainz },
{ HTTPD_METHOD_POST, "^/api/listenbrainz/token$", jsonapi_reply_listenbrainz_token_add },
{ HTTPD_METHOD_DELETE, "^/api/listenbrainz/token$", jsonapi_reply_listenbrainz_token_delete },
{ 0, NULL, NULL } { 0, NULL, NULL }
}; };

View File

@ -26,9 +26,9 @@ struct sp_credentials
char username[64]; char username[64];
char password[32]; char password[32];
uint8_t stored_cred[256]; // Actual size is 146, but leave room for some more uint8_t stored_cred[512]; // Actual size is 146, but leave room for some more
size_t stored_cred_len; size_t stored_cred_len;
uint8_t token[256]; // Actual size is ? uint8_t token[512]; // Actual size is 270 for family accounts
size_t token_len; size_t token_len;
}; };

View File

@ -142,7 +142,7 @@ session_new(struct sp_session **out, struct sp_cmdargs *cmdargs, event_callback_
if (cmdargs->stored_cred) if (cmdargs->stored_cred)
{ {
if (cmdargs->stored_cred_len > sizeof(session->credentials.stored_cred)) if (cmdargs->stored_cred_len > sizeof(session->credentials.stored_cred))
RETURN_ERROR(SP_ERR_INVALID, "Invalid stored credential"); RETURN_ERROR(SP_ERR_INVALID, "Stored credentials too long");
session->credentials.stored_cred_len = cmdargs->stored_cred_len; session->credentials.stored_cred_len = cmdargs->stored_cred_len;
memcpy(session->credentials.stored_cred, cmdargs->stored_cred, session->credentials.stored_cred_len); memcpy(session->credentials.stored_cred, cmdargs->stored_cred, session->credentials.stored_cred_len);
@ -150,7 +150,7 @@ session_new(struct sp_session **out, struct sp_cmdargs *cmdargs, event_callback_
else if (cmdargs->token) else if (cmdargs->token)
{ {
if (strlen(cmdargs->token) > sizeof(session->credentials.token)) if (strlen(cmdargs->token) > sizeof(session->credentials.token))
RETURN_ERROR(SP_ERR_INVALID, "Invalid token"); RETURN_ERROR(SP_ERR_INVALID, "Token too long");
session->credentials.token_len = strlen(cmdargs->token); session->credentials.token_len = strlen(cmdargs->token);
memcpy(session->credentials.token, cmdargs->token, session->credentials.token_len); memcpy(session->credentials.token, cmdargs->token, session->credentials.token_len);

View File

@ -84,7 +84,7 @@ param_sign(struct keyval *kv)
if (gc_err != GPG_ERR_NO_ERROR) if (gc_err != GPG_ERR_NO_ERROR)
{ {
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_LASTFM, "Could not open MD5: %s\n", ebuf); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Could not open MD5: %s\n", ebuf);
return -1; return -1;
} }
@ -99,7 +99,7 @@ param_sign(struct keyval *kv)
hash_bytes = gcry_md_read(md_hdl, GCRY_MD_MD5); hash_bytes = gcry_md_read(md_hdl, GCRY_MD_MD5);
if (!hash_bytes) if (!hash_bytes)
{ {
DPRINTF(E_LOG, L_LASTFM, "Could not read MD5 hash\n"); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Could not read MD5 hash\n");
return -1; return -1;
} }
@ -163,22 +163,22 @@ response_process(struct http_client_ctx *ctx, char **errmsg)
body = (char *)evbuffer_pullup(ctx->input_body, -1); body = (char *)evbuffer_pullup(ctx->input_body, -1);
if (!body || (strlen(body) == 0)) if (!body || (strlen(body) == 0))
{ {
DPRINTF(E_LOG, L_LASTFM, "Empty response\n"); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Empty response\n");
return -1; return -1;
} }
tree = xml_from_string(body); tree = xml_from_string(body);
if (!tree) if (!tree)
{ {
DPRINTF(E_LOG, L_LASTFM, "Failed to parse LastFM response:\n%s\n", body); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Failed to parse LastFM response:\n%s\n", body);
return -1; return -1;
} }
error = xml_get_val(tree, "lfm/error"); error = xml_get_val(tree, "lfm/error");
if (error) if (error)
{ {
DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", error); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Request to LastFM failed: %s\n", error);
DPRINTF(E_DBG, L_LASTFM, "LastFM response:\n%s\n", body); DPRINTF(E_DBG, L_SCROBBLE, "lastfm: LastFM response:\n%s\n", body);
if (errmsg) if (errmsg)
*errmsg = atrim(error); *errmsg = atrim(error);
@ -187,12 +187,12 @@ response_process(struct http_client_ctx *ctx, char **errmsg)
return -1; return -1;
} }
DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body); DPRINTF(E_SPAM, L_SCROBBLE, "lastfm: LastFM response:\n%s\n", body);
// Was it a scrobble request? Then do nothing. TODO: Check for error messages // Was it a scrobble request? Then do nothing. TODO: Check for error messages
if (xml_get_node(tree, "lfm/scrobbles/scrobble")) if (xml_get_node(tree, "lfm/scrobbles/scrobble"))
{ {
DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n"); DPRINTF(E_DBG, L_SCROBBLE, "lastfm: Scrobble callback\n");
xml_free(tree); xml_free(tree);
return 0; return 0;
} }
@ -201,12 +201,12 @@ response_process(struct http_client_ctx *ctx, char **errmsg)
sk = atrim(xml_get_val(tree, "lfm/session/key")); sk = atrim(xml_get_val(tree, "lfm/session/key"));
if (!sk) if (!sk)
{ {
DPRINTF(E_LOG, L_LASTFM, "Session key not found\n"); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Session key not found\n");
xml_free(tree); xml_free(tree);
return -1; return -1;
} }
DPRINTF(E_INFO, L_LASTFM, "Got session key from LastFM: %s\n", sk); DPRINTF(E_INFO, L_SCROBBLE, "lastfm: Got session key from LastFM: %s\n", sk);
db_admin_set(DB_ADMIN_LASTFM_SESSION_KEY, sk); db_admin_set(DB_ADMIN_LASTFM_SESSION_KEY, sk);
free(lastfm_session_key); free(lastfm_session_key);
@ -233,25 +233,27 @@ static int
request_post(const char *url, struct keyval *kv, char **errmsg) request_post(const char *url, struct keyval *kv, char **errmsg)
{ {
struct http_client_ctx ctx; struct http_client_ctx ctx;
char *request_body;
int ret; int ret;
// API requires that we MD5 sign sorted param (without "format" param) // API requires that we MD5 sign sorted param (without "format" param)
ret = param_sign(kv); ret = param_sign(kv);
if (ret < 0) if (ret < 0)
{ {
DPRINTF(E_LOG, L_LASTFM, "Aborting request, param_sign failed\n"); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Aborting request, param_sign failed\n");
return -1; return -1;
} }
memset(&ctx, 0, sizeof(struct http_client_ctx)); memset(&ctx, 0, sizeof(struct http_client_ctx));
ctx.output_body = http_form_urlencode(kv); request_body = http_form_urlencode(kv);
if (!ctx.output_body) if (!request_body)
{ {
DPRINTF(E_LOG, L_LASTFM, "Aborting request, http_form_urlencode failed\n"); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Aborting request, http_form_urlencode failed\n");
return -1; return -1;
} }
ctx.output_body = request_body;
ctx.url = url; ctx.url = url;
ctx.input_body = evbuffer_new(); ctx.input_body = evbuffer_new();
@ -262,7 +264,7 @@ request_post(const char *url, struct keyval *kv, char **errmsg)
ret = response_process(&ctx, errmsg); ret = response_process(&ctx, errmsg);
out_free_ctx: out_free_ctx:
free(ctx.output_body); free(request_body);
evbuffer_free(ctx.input_body); evbuffer_free(ctx.input_body);
return ret; return ret;
@ -281,7 +283,7 @@ scrobble(int id)
mfi = db_file_fetch_byid(id); mfi = db_file_fetch_byid(id);
if (!mfi) if (!mfi)
{ {
DPRINTF(E_LOG, L_LASTFM, "Scrobble failed, track id %d is unknown\n", id); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: Scrobble failed, track id %d is unknown\n", id);
return -1; return -1;
} }
@ -327,7 +329,7 @@ scrobble(int id)
return -1; return -1;
} }
DPRINTF(E_INFO, L_LASTFM, "Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist")); DPRINTF(E_INFO, L_SCROBBLE, "lastfm: Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist"));
ret = request_post(api_url, kv, NULL); ret = request_post(api_url, kv, NULL);
@ -367,7 +369,7 @@ lastfm_login_user(const char *user, const char *password, char **errmsg)
struct keyval *kv; struct keyval *kv;
int ret; int ret;
DPRINTF(E_LOG, L_LASTFM, "LastFM credentials file OK, logging in with username %s\n", user); DPRINTF(E_LOG, L_SCROBBLE, "lastfm: LastFM credentials file OK, logging in with username %s\n", user);
// Stop active scrobbling session // Stop active scrobbling session
stop_scrobbling(); stop_scrobbling();
@ -418,12 +420,12 @@ lastfm_logout(void)
int int
lastfm_scrobble(int id) lastfm_scrobble(int id)
{ {
DPRINTF(E_DBG, L_LASTFM, "Got LastFM scrobble request\n");
// LastFM is disabled because we already tried looking for a session key, but failed // LastFM is disabled because we already tried looking for a session key, but failed
if (lastfm_disabled) if (lastfm_disabled)
return -1; return -1;
DPRINTF(E_DBG, L_SCROBBLE, "lastfm: Got LastFM scrobble request\n");
return scrobble(id); return scrobble(id);
} }
@ -443,7 +445,7 @@ lastfm_init(void)
ret = db_admin_get(&lastfm_session_key, DB_ADMIN_LASTFM_SESSION_KEY); ret = db_admin_get(&lastfm_session_key, DB_ADMIN_LASTFM_SESSION_KEY);
if (ret < 0) if (ret < 0)
{ {
DPRINTF(E_DBG, L_LASTFM, "No valid LastFM session key\n"); DPRINTF(E_DBG, L_SCROBBLE, "lastfm: No valid LastFM session key\n");
lastfm_disabled = true; lastfm_disabled = true;
} }

327
src/listenbrainz.c Normal file
View File

@ -0,0 +1,327 @@
/*
* Copyright (C) 2025 Christian Meffert <christian.meffert@googlemail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <event2/event.h>
#include <stdbool.h>
#include <stddef.h>
#include "conffile.h"
#include "db.h"
#include "http.h"
#include "listenbrainz.h"
#include "logger.h"
#include "misc_json.h"
static const char *listenbrainz_submit_listens_url = "https://api.listenbrainz.org/1/submit-listens";
static const char *listenbrainz_validate_token_url = "https://api.listenbrainz.org/1/validate-token";
static bool listenbrainz_disabled = true;
static char *listenbrainz_token = NULL;
static time_t listenbrainz_rate_limited_until = 0;
static int
submit_listens(struct media_file_info *mfi)
{
struct http_client_ctx ctx = { 0 };
struct keyval kv_out = { 0 };
struct keyval kv_in = { 0 };
char auth_token[1024];
json_object *request_body;
json_object *listens;
json_object *listen;
json_object *track_metadata;
json_object *additional_info;
const char *x_rate_limit_reset_in;
int32_t rate_limit_seconds = -1;
int ret;
ctx.url = listenbrainz_submit_listens_url;
// Set request headers
ctx.output_headers = &kv_out;
snprintf(auth_token, sizeof(auth_token), "Token %s", listenbrainz_token);
keyval_add(ctx.output_headers, "Authorization", auth_token);
keyval_add(ctx.output_headers, "Content-Type", "application/json");
// Set request body
request_body = json_object_new_object();
json_object_object_add(request_body, "listen_type", json_object_new_string("single"));
listens = json_object_new_array();
json_object_object_add(request_body, "payload", listens);
listen = json_object_new_object();
json_object_array_add(listens, listen);
json_object_object_add(listen, "listened_at", json_object_new_int64((int64_t)time(NULL)));
track_metadata = json_object_new_object();
json_object_object_add(listen, "track_metadata", track_metadata);
json_object_object_add(track_metadata, "artist_name", json_object_new_string(mfi->artist));
json_object_object_add(track_metadata, "release_name", json_object_new_string(mfi->album));
json_object_object_add(track_metadata, "track_name", json_object_new_string(mfi->title));
additional_info = json_object_new_object();
json_object_object_add(track_metadata, "additional_info", additional_info);
json_object_object_add(additional_info, "media_player", json_object_new_string(PACKAGE_NAME));
json_object_object_add(additional_info, "media_player_version", json_object_new_string(PACKAGE_VERSION));
json_object_object_add(additional_info, "submission_client", json_object_new_string(PACKAGE_NAME));
json_object_object_add(additional_info, "submission_client_version", json_object_new_string(PACKAGE_VERSION));
json_object_object_add(additional_info, "duration_ms", json_object_new_int((int32_t)mfi->song_length));
ctx.output_body = json_object_to_json_string(request_body);
// Create input evbuffer for the response body and keyval for response headers
ctx.input_headers = &kv_in;
// Send POST request for submit-listens endpoint
ret = http_client_request(&ctx, NULL);
// Process response
if (ret < 0)
{
DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s'\n", mfi->title, mfi->artist);
goto out;
}
if (ctx.response_code == HTTP_OK)
{
DPRINTF(E_INFO, L_SCROBBLE, "lbrainz: Scrobbled '%s' by '%s'\n", mfi->title, mfi->artist);
listenbrainz_rate_limited_until = 0;
}
else if (ctx.response_code == 401)
{
DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s', unauthorized, disable scrobbling\n", mfi->title,
mfi->artist);
listenbrainz_disabled = true;
}
else if (ctx.response_code == 429)
{
x_rate_limit_reset_in = keyval_get(ctx.input_headers, "X-RateLimit-Reset-In");
ret = safe_atoi32(x_rate_limit_reset_in, &rate_limit_seconds);
if (ret == 0 && rate_limit_seconds > 0)
{
listenbrainz_rate_limited_until = time(NULL) + rate_limit_seconds;
}
DPRINTF(E_INFO, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s', rate limited for %d seconds\n", mfi->title,
mfi->artist, rate_limit_seconds);
}
else
{
DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Failed to scrobble '%s' by '%s', response code: %d\n", mfi->title, mfi->artist,
ctx.response_code);
}
out:
// Clean up
jparse_free(request_body);
keyval_clear(ctx.output_headers);
keyval_clear(ctx.input_headers);
return ret;
}
static int
validate_token(struct listenbrainz_status *status)
{
struct http_client_ctx ctx = { 0 };
struct keyval kv_out = { 0 };
char auth_token[1024];
char *response_body;
json_object *json_response = NULL;
int ret = 0;
if (!listenbrainz_token)
return -1;
ctx.url = listenbrainz_validate_token_url;
// Set request headers
ctx.output_headers = &kv_out;
snprintf(auth_token, sizeof(auth_token), "Token %s", listenbrainz_token);
keyval_add(ctx.output_headers, "Authorization", auth_token);
// Create input evbuffer for the response body
ctx.input_body = evbuffer_new();
// Send GET request for validate-token endpoint
ret = http_client_request(&ctx, NULL);
// Parse response
// 0-terminate for safety
evbuffer_add(ctx.input_body, "", 1);
response_body = (char *)evbuffer_pullup(ctx.input_body, -1);
if (!response_body || (strlen(response_body) == 0))
{
DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Request for '%s' failed, response was empty\n", ctx.url);
goto out;
}
json_response = json_tokener_parse(response_body);
if (!json_response)
DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: JSON parser returned an error for '%s'\n", ctx.url);
status->user_name = safe_strdup(jparse_str_from_obj(json_response, "user_name"));
status->token_valid = jparse_bool_from_obj(json_response, "valid");
status->message = safe_strdup(jparse_str_from_obj(json_response, "message"));
listenbrainz_disabled = !status->token_valid;
out:
// Clean up
if (ctx.input_body)
evbuffer_free(ctx.input_body);
keyval_clear(ctx.output_headers);
return ret;
}
/* Thread: worker */
int
listenbrainz_scrobble(int mfi_id)
{
struct media_file_info *mfi;
int ret;
if (listenbrainz_disabled)
return -1;
if (listenbrainz_rate_limited_until > 0 && time(NULL) < listenbrainz_rate_limited_until)
{
DPRINTF(E_INFO, L_SCROBBLE, "lbrainz: Rate limited, not scrobbling\n");
return -2;
}
mfi = db_file_fetch_byid(mfi_id);
if (!mfi)
{
DPRINTF(E_LOG, L_SCROBBLE, "lbrainz: Scrobble failed, track id %d is unknown\n", mfi_id);
return -1;
}
// Don't scrobble songs which are shorter than 30 sec
if (mfi->song_length < 30000)
goto noscrobble;
// Don't scrobble non-music and radio stations
if ((mfi->media_kind != MEDIA_KIND_MUSIC) || (mfi->data_kind == DATA_KIND_HTTP))
goto noscrobble;
// Don't scrobble songs with unknown artist
if (strcmp(mfi->artist, CFG_NAME_UNKNOWN_ARTIST) == 0)
goto noscrobble;
ret = submit_listens(mfi);
return ret;
noscrobble:
free_mfi(mfi, 0);
return -1;
}
int
listenbrainz_token_set(const char *token)
{
int ret;
if (!token)
{
DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: Failed to update ListenBrainz token, no token provided\n");
return -1;
}
ret = db_admin_set(DB_ADMIN_LISTENBRAINZ_TOKEN, token);
if (ret < 0)
{
DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: Failed to update ListenBrainz token, DB update failed\n");
}
else
{
free(listenbrainz_token);
listenbrainz_token = NULL;
ret = db_admin_get(&listenbrainz_token, DB_ADMIN_LISTENBRAINZ_TOKEN);
if (ret == 0)
listenbrainz_disabled = false;
}
return ret;
}
int
listenbrainz_token_delete(void)
{
int ret;
ret = db_admin_delete(DB_ADMIN_LISTENBRAINZ_TOKEN);
if (ret < 0)
{
DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: Failed to delete ListenBrainz token, DB delete query failed\n");
}
else
{
free(listenbrainz_token);
listenbrainz_token = NULL;
listenbrainz_disabled = true;
}
return ret;
}
int
listenbrainz_status_get(struct listenbrainz_status *status)
{
int ret = 0;
memset(status, 0, sizeof(struct listenbrainz_status));
if (listenbrainz_disabled)
{
status->disabled = true;
}
else
{
ret = validate_token(status);
}
return ret;
}
void
listenbrainz_status_free(struct listenbrainz_status *status, bool content_only)
{
free(status->user_name);
free(status->message);
if (!content_only)
free(status);
}
/* Thread: main */
int
listenbrainz_init(void)
{
int ret;
ret = db_admin_get(&listenbrainz_token, DB_ADMIN_LISTENBRAINZ_TOKEN);
listenbrainz_disabled = (ret < 0);
if (listenbrainz_disabled)
{
DPRINTF(E_DBG, L_SCROBBLE, "lbrainz: No valid ListenBrainz token\n");
}
return 0;
}

25
src/listenbrainz.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef __LISTENBRAINZ_H__
#define __LISTENBRAINZ_H__
struct listenbrainz_status {
bool disabled;
char *user_name;
bool token_valid;
char *message;
};
int
listenbrainz_scrobble(int mfi_id);
int
listenbrainz_token_set(const char *token);
int
listenbrainz_token_delete(void);
int
listenbrainz_status_get(struct listenbrainz_status *status);
void
listenbrainz_status_free(struct listenbrainz_status *status, bool content_only);
int
listenbrainz_init(void);
#endif /* !__LISTENBRAINZ_H__ */

View File

@ -58,7 +58,7 @@ static uint32_t logger_repeat_counter;
static uint32_t logger_last_hash; static uint32_t logger_last_hash;
static char *logfilename; static char *logfilename;
static FILE *logfile; static FILE *logfile;
static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd", "stream", "cast", "fifo", "lib", "web", "airplay", "rcp" }; static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "scrobble", "cache", "mpd", "stream", "cast", "fifo", "lib", "web", "airplay", "rcp" };
static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" }; static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" };

View File

@ -28,7 +28,7 @@
#define L_DMAP 19 #define L_DMAP 19
#define L_DBPERF 20 #define L_DBPERF 20
#define L_SPOTIFY 21 #define L_SPOTIFY 21
#define L_LASTFM 22 #define L_SCROBBLE 22
#define L_CACHE 23 #define L_CACHE 23
#define L_MPD 24 #define L_MPD 24
#define L_STREAMING 25 #define L_STREAMING 25

View File

@ -72,6 +72,7 @@
#ifdef LASTFM #ifdef LASTFM
# include "lastfm.h" # include "lastfm.h"
#endif #endif
#include "listenbrainz.h"
#define PIDFILE STATEDIR "/run/" PACKAGE ".pid" #define PIDFILE STATEDIR "/run/" PACKAGE ".pid"
#define WEB_ROOT DATADIR "/htdocs" #define WEB_ROOT DATADIR "/htdocs"
@ -833,6 +834,7 @@ main(int argc, char **argv)
#ifdef LASTFM #ifdef LASTFM
lastfm_init(); lastfm_init();
#endif #endif
listenbrainz_init();
/* Start Remote pairing service */ /* Start Remote pairing service */
ret = remote_pairing_init(); ret = remote_pairing_init();

View File

@ -91,6 +91,7 @@
#ifdef LASTFM #ifdef LASTFM
# include "lastfm.h" # include "lastfm.h"
#endif #endif
#include "listenbrainz.h"
// The interval between each tick of the playback clock in ms. This means that // The interval between each tick of the playback clock in ms. This means that
// we read 10 ms frames from the input and pass to the output, so the clock // we read 10 ms frames from the input and pass to the output, so the clock
@ -378,16 +379,17 @@ skipcount_inc_cb(void *arg)
db_file_inc_skipcount(*id); db_file_inc_skipcount(*id);
} }
#ifdef LASTFM
// Callback from the worker thread (async operation as it may block) // Callback from the worker thread (async operation as it may block)
static void static void
scrobble_cb(void *arg) scrobble_cb(void *arg)
{ {
int *id = arg; int *id = arg;
#ifdef LASTFM
lastfm_scrobble(*id); lastfm_scrobble(*id);
}
#endif #endif
listenbrainz_scrobble(*id);
}
// This is just to be able to log the caller in a simple way // This is just to be able to log the caller in a simple way
#define status_update(x, y) status_update_impl((x), (y), __func__) #define status_update(x, y) status_update_impl((x), (y), __func__)
@ -1072,9 +1074,7 @@ event_play_eof()
if (id != DB_MEDIA_FILE_NON_PERSISTENT_ID) if (id != DB_MEDIA_FILE_NON_PERSISTENT_ID)
{ {
worker_execute(playcount_inc_cb, &id, sizeof(int), 5); worker_execute(playcount_inc_cb, &id, sizeof(int), 5);
#ifdef LASTFM
worker_execute(scrobble_cb, &id, sizeof(int), 8); worker_execute(scrobble_cb, &id, sizeof(int), 8);
#endif
history_add(pb_session.playing_now->id, pb_session.playing_now->item_id); history_add(pb_session.playing_now->id, pb_session.playing_now->item_id);
} }