mirror of
https://github.com/owntone/owntone-server.git
synced 2025-03-03 23:30:09 -05:00
[sync] Merge remote-tracking branch 'origin/master' into bulma-1.0
This commit is contained in:
commit
e9ed220853
249
ChangeLog
249
ChangeLog
@ -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 Catalina’s Apple Music
|
- fix for macOS Catalina’s 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.
|
||||||
|
@ -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
1
docs/changelog.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
--8<-- "ChangeLog"
|
File diff suppressed because one or more lines are too long
25
mkdocs.yml
25
mkdocs.yml
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 \
|
||||||
|
1
src/db.h
1
src/db.h
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
44
src/lastfm.c
44
src/lastfm.c
@ -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
327
src/listenbrainz.c
Normal 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
25
src/listenbrainz.h
Normal 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__ */
|
@ -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" };
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user