From e4751a5f39059a0b88251eb646a4ea2ccb004ac2 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Wed, 11 Dec 2024 11:40:19 +0100 Subject: [PATCH 01/11] Revert this. --- src/libs/http.liq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/http.liq b/src/libs/http.liq index cece16dd1b..1ef9cc529b 100644 --- a/src/libs/http.liq +++ b/src/libs/http.liq @@ -964,7 +964,7 @@ def http.headers.content_disposition(headers) = type: string, filename?: string, name?: string, - args: [(string * string?)] + args: [(string*string?)] } ) end, From b32dc042d4a39d039d036bd0ccde23859cccf7e7 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Mon, 9 Dec 2024 17:39:43 -0600 Subject: [PATCH 02/11] Remove external lastfm. --- dune-project | 2 - liquidsoap-core.opam | 2 - src/config/lastfm_option.disabled.ml | 1 - src/config/lastfm_option.enabled.ml | 1 - src/core/builtins/builtins_lastfm.ml | 110 ----------- src/core/builtins/builtins_optionals.ml | 1 - src/core/dune | 14 -- src/core/tools/liqfm.ml | 244 ------------------------ src/runtime/build_config.ml | 1 - 9 files changed, 376 deletions(-) delete mode 120000 src/config/lastfm_option.disabled.ml delete mode 120000 src/config/lastfm_option.enabled.ml delete mode 100644 src/core/builtins/builtins_lastfm.ml delete mode 100644 src/core/tools/liqfm.ml diff --git a/dune-project b/dune-project index b1f1a11f16..7b1be7b7f6 100644 --- a/dune-project +++ b/dune-project @@ -83,7 +83,6 @@ jemalloc ladspa lame - lastfm lilv lo mad @@ -123,7 +122,6 @@ (inotify (< 1.0)) (ladspa (< 0.2.0)) (lame (< 0.3.7)) - (lastfm (< 0.3.4)) (lo (< 0.2.0)) (mad (< 0.5.0)) (magic (< 0.6)) diff --git a/liquidsoap-core.opam b/liquidsoap-core.opam index 9f33a5b40e..6a75736720 100644 --- a/liquidsoap-core.opam +++ b/liquidsoap-core.opam @@ -48,7 +48,6 @@ depopts: [ "jemalloc" "ladspa" "lame" - "lastfm" "lilv" "lo" "mad" @@ -89,7 +88,6 @@ conflicts: [ "inotify" {< "1.0"} "ladspa" {< "0.2.0"} "lame" {< "0.3.7"} - "lastfm" {< "0.3.4"} "lo" {< "0.2.0"} "mad" {< "0.5.0"} "magic" {< "0.6"} diff --git a/src/config/lastfm_option.disabled.ml b/src/config/lastfm_option.disabled.ml deleted file mode 120000 index 370c3e56d3..0000000000 --- a/src/config/lastfm_option.disabled.ml +++ /dev/null @@ -1 +0,0 @@ -noop.disabled.ml \ No newline at end of file diff --git a/src/config/lastfm_option.enabled.ml b/src/config/lastfm_option.enabled.ml deleted file mode 120000 index 34bd7cbe43..0000000000 --- a/src/config/lastfm_option.enabled.ml +++ /dev/null @@ -1 +0,0 @@ -noop.enabled.ml \ No newline at end of file diff --git a/src/core/builtins/builtins_lastfm.ml b/src/core/builtins/builtins_lastfm.ml deleted file mode 100644 index dd743b3b09..0000000000 --- a/src/core/builtins/builtins_lastfm.ml +++ /dev/null @@ -1,110 +0,0 @@ -(***************************************************************************** - - Liquidsoap, a programmable stream generator. - Copyright 2003-2024 Savonet team - - 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, fully stated in the COPYING - file at the root of the liquidsoap distribution. - - 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - - *****************************************************************************) - -let audioscrobbler = Lang.add_module "audioscrobbler" -let log = Log.make ["lastfm"; "submit"] - -let () = - let f name stype descr = - let proto = - [ - ("user", Lang.string_t, None, None); - ("password", Lang.string_t, None, None); - ( "host", - Lang.string_t, - Some (Lang.string !Liqfm.Audioscrobbler.base_host), - Some "Host for audioscrobbling submissions." ); - ( "port", - Lang.int_t, - Some (Lang.int !Liqfm.Audioscrobbler.base_port), - Some "Port for audioscrobbling submissions." ); - ( "length", - Lang.bool_t, - Some (Lang.bool false), - Some - "Try to submit length information. This operation can be CPU \ - intensive. Value forced to true when used with the \"user\" \ - source type." ); - ("", Lang.metadata_t, None, None); - ] - in - let proto = - if stype = Liqfm.Played then - ( "source", - Lang.string_t, - Some (Lang.string "broadcast"), - Some - "Source for tracks. Should be one of: \"broadcast\", \"user\", \ - \"recommendation\" or \"unknown\". Since liquidsoap is intended \ - for radio broadcasting, this is the default. Sources other than \ - user don't need duration to be set." ) - :: proto - else proto - in - let tasks = Hashtbl.create 1 in - ignore - (Lang.add_builtin ~base:audioscrobbler name - ~category:`Interaction (* TODO better cat *) ~descr proto Lang.unit_t - (fun p -> - let user = Lang.to_string (List.assoc "user" p) in - let password = Lang.to_string (List.assoc "password" p) in - let metas = Lang.to_metadata (Lang.assoc "" 1 p) in - let host = Lang.to_string (List.assoc "host" p) in - let port = Lang.to_int (List.assoc "port" p) in - let host = (host, port) in - let mode = - if stype = Liqfm.Played then ( - match Lang.to_string (List.assoc "source" p) with - | "broadcast" -> Liqfm.Broadcast - | "user" -> Liqfm.User - | "recommendation" -> Liqfm.Recommendation - | "unknown" -> Liqfm.Unknown - | _ -> - raise - (Error.Invalid_value - ( List.assoc "source" p, - "unknown lastfm submission mode" ))) - else Liqfm.Unknown - in - let length = Lang.to_bool (List.assoc "length" p) in - let length = - if length = false && mode = Liqfm.User then ( - log#severe - "length information is required for \"user\" sources, setting \ - to true."; - true) - else length - in - let task = - try Hashtbl.find tasks host - with Not_found -> - let t = Liqfm.init host in - Hashtbl.replace tasks host t; - t - in - Liqfm.submit (user, password) task length mode stype [metas]; - Lang.unit)) - in - f "submit" Liqfm.Played - "Submit a played song using the audioscrobbler protocol."; - f "nowplaying" Liqfm.NowPlaying - "Submit a now playing song using the audioscrobbler protocol." diff --git a/src/core/builtins/builtins_optionals.ml b/src/core/builtins/builtins_optionals.ml index 8544b6532f..bf0cee5700 100644 --- a/src/core/builtins/builtins_optionals.ml +++ b/src/core/builtins/builtins_optionals.ml @@ -28,7 +28,6 @@ let () = ("irc", Irc_option.enabled); ("ladspa", Ladspa_option.enabled); ("lame", Lame_option.enabled); - ("lastfm", Lastfm_option.enabled); ("lilv", Lilv_option.enabled); ("lo", Lo_option.enabled); ("mad", Mad_option.enabled); diff --git a/src/core/dune b/src/core/dune index 6933405e89..111931789c 100644 --- a/src/core/dune +++ b/src/core/dune @@ -465,14 +465,6 @@ (optional) (modules lame_encoder)) -(library - (name liquidsoap_lastfm) - (libraries lastfm liquidsoap_core) - (library_flags -linkall) - (wrapped false) - (optional) - (modules builtins_lastfm liqfm)) - (library (name liquidsoap_lilv) (libraries lilv liquidsoap_core) @@ -737,7 +729,6 @@ jemalloc_option ladspa_option lame_option - lastfm_option lilv_option lo_option mad_option @@ -858,11 +849,6 @@ from (liquidsoap_lame -> lame_option.enabled.ml) (-> lame_option.disabled.ml)) - (select - lastfm_option.ml - from - (liquidsoap_lastfm -> lastfm_option.enabled.ml) - (-> lastfm_option.disabled.ml)) (select lilv_option.ml from diff --git a/src/core/tools/liqfm.ml b/src/core/tools/liqfm.ml deleted file mode 100644 index d4c0eeddad..0000000000 --- a/src/core/tools/liqfm.ml +++ /dev/null @@ -1,244 +0,0 @@ -(***************************************************************************** - - Liquidsoap, a programmable stream generator. - Copyright 2003-2024 Savonet team - - 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, fully stated in the COPYING - file at the root of the liquidsoap distribution. - - 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - - *****************************************************************************) - -(* A custom implementation of HTTP - * requests. *) -module Liq_http = struct - type request = Get | Post of string - - exception Http of string - - let exc_of_exc = function - | Http s -> Http s - | e -> Http (Printexc.to_string e) - - (* This in unused for now.. *) - let default_timeout = ref 50. - - let request ?timeout ?headers ?(port = 80) ~host ~url ~request () = - try - let timeout = - match timeout with None -> !default_timeout | Some t -> t - in - let mk_read s = - let buf = Buffer.create 10 in - Buffer.add_string buf s; - fun len -> - let len = min (Buffer.length buf) len in - let ret = Buffer.sub buf 0 len in - Utils.buffer_drop buf len; - ret - in - let request = - match request with - | Get -> `Get - | Post s -> `Post (Some (Int64.of_int (String.length s)), mk_read s) - in - let url = Printf.sprintf "http://%s:%d%s" host port url in - let data = Buffer.create 1024 in - let x, code, y, _ = - Liqcurl.http_request ?headers ~pos:[] ~follow_redirect:true - ~on_body_data:(Buffer.add_string data) - ~timeout:(Some (int_of_float (timeout *. 1000.))) - ~url ~request () - in - if code <> 200 then - raise (Http (Printf.sprintf "Http request failed: %s %i %s" x code y)); - Buffer.contents data - with e -> raise (exc_of_exc e) -end - -module Audioscrobbler = Lastfm_generic.Audioscrobbler_generic (Liq_http) - -let error_translator = function - | Audioscrobbler.Error x -> - Some - (Printf.sprintf "Audioscrobbler error: %s" - (Audioscrobbler.string_of_error x)) - | _ -> None - -let () = Printexc.register_printer error_translator - -open Lastfm_generic -open Audioscrobbler - -type source = User | Lastfm | Broadcast | Recommendation | Unknown -type submission = NowPlaying | Played - -type task = { - task : Duppy.Async.t; - submit_m : Mutex.t; - submissions : - (string * string * source * submission * bool * Frame.metadata) Queue.t; -} - -let log = Log.make ["audioscrobbler"] - -exception Duration - -let client = { client = "lsp"; version = "0.1" } - -let init host = - (* The list of waiting submissions *) - let submissions = Queue.create () in - (* A mutex to manage thread concurrency *) - let submit_m = Mutex.create () in - let reason s = log#important "Lastfm Submission failed: %s" s in - (* Define a new task *) - let do_submit () = - try - (* This function checks that the submission is valid *) - let song songs (user, password, (source : source), stype, length, m) = - let login = { user; password } in - let f x = try Frame.Metadata.find x m with Not_found -> "" in - let artist, track = (f "artist", f "title") in - let s = - match stype with Played -> "submit" | NowPlaying -> "nowplaying" - in - let h, p = host in - log#info "Submitting %s -- %s with mode: %s to %s:%i" artist track s h p; - try - let duration () = - try - match float_of_string_opt (Frame.Metadata.find "duration" m) with - | Some d -> d - | None -> raise Not_found - with Not_found -> ( - let exception Bad_rid in - try - let rid = - match int_of_string_opt (Frame.Metadata.find "rid" m) with - | Some rid -> rid - | None -> raise Bad_rid - in - let request = Request.from_id rid in - match request with - | Some s -> ( - match Request.get_filename s with - | Some file -> ( - match - Request.duration ~metadata:(Request.metadata s) - file - with - | Some f -> f - | None -> raise Not_found) - | None -> raise Not_found) - | None -> raise Not_found - with - | Not_found -> raise Duration - | Bad_rid -> - log#severe "Metadata 'rid' is not associated to an integer!"; - raise Duration) - in - let duration = - if length then ( - try Some (duration ()) - with Duration -> if source = User then raise Duration else None) - else if source <> User then None - else raise Duration - in - let time = Unix.time () in - let trackauth = - (* Only when source is lasftm *) - match source with - | Lastfm -> Some (f "lastfm:trackauth") - | _ -> None - in - let source = - match source with - | User -> Audioscrobbler.User - | Lastfm -> Audioscrobbler.Lastfm - | Broadcast -> Audioscrobbler.Broadcast - | Recommendation -> Audioscrobbler.Recommendation - | Unknown -> Audioscrobbler.Unknown - in - let song = - { - artist; - track; - time = Some time; - source = Some source; - rating = None; - length = duration; - album = Some (f "album"); - tracknumber = None; - musicbrainzid = None; - trackauth; - } - in - check_song song Submit; - (login, stype, song) :: songs - with - | Duration -> - log#info "could not submit track %s -- %s, no duration available" - artist track; - songs - | Error e -> - log#info "could not submit track %s -- %s, %s" artist track - (string_of_error e); - songs - | e -> - log#info "could not submit track %s -- %s: unknown error %s" - artist track (Printexc.to_string e); - songs - in - Mutex.lock submit_m; - let songs = Queue.fold song [] submissions in - Queue.clear submissions; - Mutex.unlock submit_m; - let submit = Hashtbl.create 10 in - let filter (c, t, m) = - try - let v = Hashtbl.find submit (c, t) in - Hashtbl.replace submit (c, t) (m :: v) - with Not_found -> Hashtbl.replace submit (c, t) [m] - in - List.iter filter songs; - let f (login, (stype : submission)) songs = - try - match stype with - | NowPlaying -> - List.iter - (fun song -> Audioscrobbler.do_np ~host client login song) - songs - | Played -> - ignore (Audioscrobbler.do_submit ~host client login songs) - with Audioscrobbler.Error e -> - reason (Audioscrobbler.string_of_error e) - in - Hashtbl.iter f submit; - -1. - with e -> - reason (Printexc.to_string e); - -1. - in - let task = Duppy.Async.add ~priority:`Blocking Tutils.scheduler do_submit in - { task; submit_m; submissions } - -let submit (user, password) task length source stype songs = - let songs = - List.map (fun x -> (user, password, source, stype, length, x)) songs - in - Mutex.lock task.submit_m; - List.iter (fun x -> Queue.add x task.submissions) songs; - Mutex.unlock task.submit_m; - Duppy.Async.wake_up task.task diff --git a/src/runtime/build_config.ml b/src/runtime/build_config.ml index 20495b0da7..206dfbacde 100644 --- a/src/runtime/build_config.ml +++ b/src/runtime/build_config.ml @@ -121,7 +121,6 @@ let build_config = - inotify : %{Inotify_option.detected} - irc : %{Irc_option.detected} - jemalloc : %{Jemalloc_option.detected} - - lastfm : %{Lastfm_option.detected} - lo : %{Lo_option.detected} - memtrace : %{Memtrace_option.detected} - osc : %{Osc_option.detected} From e6bb08d81047da10a9e0ec0679403cee42ad087a Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Mon, 9 Dec 2024 23:34:35 -0600 Subject: [PATCH 03/11] Add new audioscrobbler implementation. --- src/libs/extra/audioscrobbler.liq | 398 ++++++++++++++++++++++++++++++ src/libs/extra/lastfm.liq | 185 -------------- src/libs/http.liq | 17 ++ src/libs/stdlib.liq | 2 +- 4 files changed, 416 insertions(+), 186 deletions(-) create mode 100644 src/libs/extra/audioscrobbler.liq delete mode 100644 src/libs/extra/lastfm.liq diff --git a/src/libs/extra/audioscrobbler.liq b/src/libs/extra/audioscrobbler.liq new file mode 100644 index 0000000000..6888132945 --- /dev/null +++ b/src/libs/extra/audioscrobbler.liq @@ -0,0 +1,398 @@ +let error.audioscrobbler = error.register("audioscrobbler") + +let settings.audioscrobbler = + settings.make.void( + "Audioscrobbler settings" + ) + +let settings.audioscrobbler.api_key = + settings.make( + description= + "Default API key for audioscrobbler", + "" + ) + +let settings.audioscrobbler.api_secret = + settings.make( + description= + "Default API secret for audioscrobbler", + "" + ) + +audioscrobbler = () + +def audioscrobbler.request( + ~base_url="http://ws.audioscrobbler.com/2.0", + ~api_key=null(), + ~api_secret=null(), + params +) = + api_key = api_key ?? settings.audioscrobbler.api_key() + api_secret = api_secret ?? settings.audioscrobbler.api_secret() + + if + api_key == "" or api_secret == "" + then + error.raise( + error.audioscrobbler, + "`api_key` or `api_secret` missing!" + ) + end + + params = [("api_key", api_key), ...params] + + sig_params = list.sort(fun (v, v') -> string.compare(fst(v), fst(v')), params) + sig_params = list.map(fun (v) -> "#{fst(v)}#{(snd(v) : string)}", sig_params) + sig_params = string.concat(separator="", sig_params) + + api_sig = string.digest("#{sig_params}#{api_secret}") + + http.post( + base_url, + headers=[("Content-Type", "application/x-www-form-urlencoded")], + data=http.www_form_urlencoded([...params, ("api_sig", api_sig)]) + ) +end + +def audioscrobbler.check_response(resp) = + let xml.parse ({lfm = {xml_params = {status}}} : + {lfm: {xml_params: {status: string}}} + ) = resp + if + (status == "failed") + then + error_ref = error + let xml.parse ({lfm = {error = {xml_params = {code}}}} : + {lfm: {error: string.{ xml_params: {code: int} }}} + ) = resp + error_ref.raise( + error_ref.audioscrobbler, + "Error #{code}: #{error}" + ) + end +end + +def audioscrobbler.auth( + ~username, + ~password, + ~api_key=null(), + ~api_secret=null() +) = + resp = + audioscrobbler.request( + api_key=api_key, + api_secret=api_secret, + [ + ("method", "auth.getMobileSession"), + ("username", username), + ("password", password) + ] + ) + + audioscrobbler.check_response(resp) + + try + let xml.parse ({lfm = {session = {name, key}}} : + {lfm: {session: {name: string, key: string}}} + ) = resp + assert(name == username) + key + catch err do + error.raise( + error.invalid, + "Invalid response: #{resp}, error: #{err}" + ) + end +end + +let audioscrobbler.api = {track=()} + +# Submit a track to the audioscrobbler +# `track.updateNowPlaying` API. +# @category Interaction +def audioscrobbler.api.track.updateNowPlaying( + ~username, + ~password, + ~session_key=null(), + ~api_key=null(), + ~api_secret=null(), + ~artist, + ~track, + ~album=null(), + ~context=null(), + ~trackNumber=null(), + ~mbid=null(), + ~albumArtist=null(), + ~duration=null() +) = + session_key = + session_key + ?? + audioscrobbler.auth( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret + ) + + params = + [ + ("track", track), + ("artist", artist), + ...(null.defined(album) ? [("album", null.get(album))] : [] ), + ...(null.defined(context) ? [("context", null.get(context))] : [] ), + ...( + null.defined(trackNumber) + ? [("trackNumber", string((null.get(trackNumber) : int)))] : [] + ), + ...(null.defined(mbid) ? [("mbid", null.get(mbid))] : [] ), + ...( + null.defined(albumArtist) + ? [("albumArtist", null.get(albumArtist))] : [] + ), + ...( + null.defined(duration) + ? [("duration", string((null.get(duration) : int)))] : [] + ) + ] + + log.info( + label="audioscrobbler.api.track.updateNowPlaying", + "Submitting updateNowPlaying with: #{params}" + ) + + resp = + audioscrobbler.request( + api_key=api_key, + api_secret=api_secret, + [...params, ("method", "track.updateNowPlaying"), ("sk", session_key)] + ) + + audioscrobbler.check_response(resp) + + try + let xml.parse (v : + { + lfm: { + nowplaying: { + track: string.{ xml_params: {corrected: string} }, + artist: string.{ xml_params: {corrected: string} }, + album: string?.{ xml_params: {corrected: string} }, + albumArtist: string?.{ xml_params: {corrected: string} }, + timestamp: string, + ignoredMessage: {xml_params: {code: string}} + }, + xml_params: {status: string} + } + } + ) = resp + + log.info( + label="audioscrobbler.api.track.updateNowPlaying", + "Done submitting updateNowPlaying with: #{params}" + ) + + v + catch err do + error.raise( + error.invalid, + "Invalid response: #{resp}, error: #{err}" + ) + end +end + +# Submit a track to the audioscrobbler +# `track.scrobble` API. +# @category Interaction +def audioscrobbler.api.track.scrobble( + ~username, + ~password, + ~session_key=null(), + ~api_key=null(), + ~api_secret=null(), + ~artist, + ~track, + ~timestamp=time(), + ~album=null(), + ~context=null(), + ~streamId=null(), + ~chosenByUser=true, + ~trackNumber=null(), + ~mbid=null(), + ~albumArtist=null(), + ~duration=null() +) = + session_key = + session_key + ?? + audioscrobbler.auth( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret + ) + + params = + [ + ("track", track), + ("artist", artist), + ("timestamp", string(timestamp)), + ...(null.defined(album) ? [("album", null.get(album))] : [] ), + ...(null.defined(context) ? [("context", null.get(context))] : [] ), + ...(null.defined(streamId) ? [("streamId", null.get(streamId))] : [] ), + + ("chosenByUser", chosenByUser ? "1" : "0" ), + ...( + null.defined(trackNumber) + ? [("trackNumber", string((null.get(trackNumber) : int)))] : [] + ), + ...(null.defined(mbid) ? [("mbid", null.get(mbid))] : [] ), + ...( + null.defined(albumArtist) + ? [("albumArtist", null.get(albumArtist))] : [] + ), + ...( + null.defined(duration) + ? [("duration", string((null.get(duration) : int)))] : [] + ) + ] + + log.info( + label="audioscrobbler.api.track.scrobble", + "Submitting updateNowPlaying with: #{params}" + ) + + resp = + audioscrobbler.request( + api_key=api_key, + api_secret=api_secret, + [...params, ("method", "track.scrobble"), ("sk", session_key)] + ) + + audioscrobbler.check_response(resp) + + try + let xml.parse (v : + { + lfm: { + scrobbles: { + scrobble: { + track: string.{ xml_params: {corrected: string} }, + artist: string.{ xml_params: {corrected: string} }, + album: string?.{ xml_params: {corrected: string} }, + albumArtist: string?.{ xml_params: {corrected: string} }, + timestamp: string, + ignoredMessage: {xml_params: {code: string}} + }, + xml_params: {ignored: string, accepted: string} + }, + xml_params: {status: string} + } + } + ) = resp + + log.info( + label="audioscrobbler.api.track.scrobble", + "Done submitting updateNowPlaying with: #{params}" + ) + + v + catch err do + error.raise( + error.invalid, + "Invalid response: #{resp}, error: #{err}" + ) + end +end + +let source_on_end = source.on_end +let source_on_metadata = source.on_metadata + +# Submit songs using audioscrobbler, respecting the full protocol: +# First signal song as now playing when starting, and +# then submit song when it ends. +# @category Interaction +# @flag extra +# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set. +# @param ~delay Submit song when there is only this delay left, in seconds. +# @param ~force If remaining time is null, the song will be assumed to be skipped or cut, and not submitted. Set this to `true` to prevent this behavior +def audioscrobbler.submit( + ~username, + ~password, + ~api_key=null(), + ~api_secret=null(), + ~delay=10., + ~force=false, + s +) = + session_key = + audioscrobbler.auth( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret + ) + + def apply_meta(fn, m) = + def c(v) = + v == "" ? null() : v + end + track = m["title"] + album = c(m["album"]) + artist = m["artist"] + trackNumber = + try + null.map(int_of_string, c(m["tracknumber"])) + catch _ do + null() + end + albumArtist = c(m["albumartist"]) + ignore( + fn( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret, + session_key=session_key, + track=track, + artist=artist, + album=album, + trackNumber=trackNumber, + albumArtist=albumArtist + ) + ) + end + + def now_playing(m) = + try + apply_meta(audioscrobbler.api.track.updateNowPlaying, m) + catch err do + log.important( + "Error while submitting nowplaying info for #{source.id(s)}: #{err}" + ) + end + end + + s = source_on_metadata(s, now_playing) + f = + fun (rem, m) -> + # Avoid skipped songs + if + rem > 0. or force + then + try + apply_meta(audioscrobbler.api.track.scrobble, m) + catch err do + log.important( + "Error while submitting scrobble info for #{source.id(s)}: #{err}" + ) + end + else + log( + label="audioscrobbler.submit", + level=4, + "Remaining time null: will not submit song (song skipped ?)" + ) + end + source_on_end(s, delay=delay, f) +end diff --git a/src/libs/extra/lastfm.liq b/src/libs/extra/lastfm.liq deleted file mode 100644 index 8e2405c0d4..0000000000 --- a/src/libs/extra/lastfm.liq +++ /dev/null @@ -1,185 +0,0 @@ -%ifdef audioscrobbler.submit -librefm = () -lastfm = () - -# Submit metadata to libre.fm using the audioscrobbler protocol. -# @category Interaction -# @flag extra -# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set. -# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type. -def librefm.submit(~user, ~password, ~source="broadcast", ~length=false, m) = - audioscrobbler.submit( - user=user, - password=password, - source=source, - length=length, - host="turtle.libre.fm", - port=80, - m - ) -end - -# Submit metadata to lastfm.fm using the audioscrobbler protocol. -# @category Interaction -# @flag extra -# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set. -# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type. -def lastfm.submit(~user, ~password, ~source="broadcast", ~length=false, m) = - audioscrobbler.submit( - user=user, - password=password, - source=source, - length=length, - host="post.audioscrobbler.com", - port=80, - m - ) -end - -# Submit metadata to libre.fm using the audioscrobbler protocol (nowplaying mode). -# @category Interaction -# @flag extra -# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type. -def librefm.nowplaying(~user, ~password, ~length=false, m) = - audioscrobbler.nowplaying( - user=user, - password=password, - length=length, - host="turtle.libre.fm", - port=80, - m - ) -end - -# Submit metadata to lastfm.fm using the audioscrobbler protocol (nowplaying mode). -# @category Interaction -# @flag extra -# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type. -def lastfm.nowplaying(~user, ~password, ~length=false, m) = - audioscrobbler.nowplaying( - user=user, - password=password, - length=length, - host="post.audioscrobbler.com", - port=80, - m - ) -end - -let source_on_end = source.on_end -let source_on_metadata = source.on_metadata - -# Submit songs using audioscrobbler, respecting the full protocol: -# First signal song as now playing when starting, and -# then submit song when it ends. -# @category Interaction -# @flag extra -# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set. -# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type. -# @param ~delay Submit song when there is only this delay left, in seconds. -# @param ~force If remaining time is null, the song will be assumed to be skipped or cut, and not submitted. Set to zero to disable this behaviour. -def audioscrobbler.submit.full( - ~user, - ~password, - ~host="post.audioscrobbler.com", - ~port=80, - ~source="broadcast", - ~length=false, - ~delay=10., - ~force=false, - s -) = - def f(m) = - audioscrobbler.nowplaying( - user=user, password=password, host=host, port=port, length=length, m - ) - end - - s = source_on_metadata(s, f) - f = - fun (rem, m) -> - # Avoid skipped songs - if - rem > 0. or force - then - audioscrobbler.submit( - user=user, - password=password, - host=host, - port=port, - length=length, - source=source, - m - ) - else - log( - label="audioscrobbler.submit.full", - level=4, - "Remaining time null: will not submit song (song skipped ?)" - ) - end - source_on_end(s, delay=delay, f) -end - -# Submit songs to librefm using audioscrobbler, respecting the full protocol: -# First signal song as now playing when starting, and -# then submit song when it ends. -# @category Interaction -# @flag extra -# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set. -# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type. -# @param ~delay Submit song when there is only this delay left, in seconds. If remaining time is less than this value, the song will be assumed to be skipped or cut, and not submitted. Set to zero to disable this behaviour. -# @param ~force If remaining time is null, the song will be assumed to be skipped or cut, and not submitted. Set to zero to disable this behaviour. -def librefm.submit.full( - ~user, - ~password, - ~source="broadcast", - ~length=false, - ~delay=10., - ~force=false, - s -) = - audioscrobbler.submit.full( - user=user, - password=password, - source=source, - length=length, - host="turtle.libre.fm", - port=80, - delay=delay, - force=force, - s - ) -end - -# Submit songs to lastfm using audioscrobbler, respecting the full protocol: -# First signal song as now playing when starting, and -# then submit song when it ends. -# @category Interaction -# @flag extra -# @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set. -# @param ~length Try to submit length information. This operation can be CPU intensive. Value forced to true when used with the "user" source type. -# @param ~delay Submit song when there is only this delay left, in seconds. If remaining time is less than this value, the song will be assumed to be skipped or cut, and not submitted. Set to zero to disable this behaviour. -# @param ~force If remaining time is null, the song will be assumed to be skipped or cut, and not submitted. Set to zero to disable this behaviour. -def lastfm.submit.full( - ~user, - ~password, - ~source="broadcast", - ~length=false, - ~delay=10., - ~force=false, - s -) = - audioscrobbler.submit.full( - user=user, - password=password, - source=source, - length=length, - host="post.audioscrobbler.com", - port=80, - delay=delay, - force=force, - s - ) -end -%endif diff --git a/src/libs/http.liq b/src/libs/http.liq index 1ef9cc529b..c91afd3f74 100644 --- a/src/libs/http.liq +++ b/src/libs/http.liq @@ -1,5 +1,22 @@ # Set of HTTP utils. +# Prepare a list of `(string, string)` arguments for +# sending as `"application/x-www-form-urlencoded"` content +# @category Internet +def http.www_form_urlencoded(params) = + params = + list.map( + fun (v) -> + begin + let (key, value) = v + "#{url.encode(key)}=#{url.encode(value)}" + end, + params + ) + + string.concat(separator="&", params) +end + # Prepare a list of data to be sent as multipart form data. # @category Internet # @param ~boundary Specify boundary to use for multipart/form-data. diff --git a/src/libs/stdlib.liq b/src/libs/stdlib.liq index dacb255f90..51f708e55e 100644 --- a/src/libs/stdlib.liq +++ b/src/libs/stdlib.liq @@ -48,7 +48,7 @@ %include_extra "extra/source.liq" %include_extra "extra/http.liq" %include_extra "extra/externals.liq" -%include_extra "extra/lastfm.liq" +%include_extra "extra/audioscrobbler.liq" %include_extra "extra/server.liq" %include_extra "extra/telnet.liq" %include_extra "extra/interactive.liq" From 04a69d1b8bf5b9b1f6f62c4feffebc4647a9508a Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Tue, 10 Dec 2024 00:39:23 -0600 Subject: [PATCH 04/11] Switch back to value, filter accorridng to type. --- :Format | 64 +++++++++++++++++++++++++++++++ src/libs/extra/audioscrobbler.liq | 26 ++++++------- 2 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 :Format diff --git a/:Format b/:Format new file mode 100644 index 0000000000..807d125286 --- /dev/null +++ b/:Format @@ -0,0 +1,64 @@ +let rec methods_of_xml = function + | Xml.PCData s -> ("text", Lang.string s) + | Xml.Element (name, params, ([Xml.PCData s] as children)) -> + (name, Lang.meth (Lang.string s) (xml_node ~params ~children)) + | Xml.Element (name, params, children) -> + ( name, + Lang.record ( + (List.fold_left + (fun methods el -> + let (name, v) = methods_of_xml el in + let v = + match Methods.find_opt name methods with + | None -> v + | Some (`Tuple {value}) -> Value.make (`Tuple (value @[v])) + | Some value -> (name, Value.make (`Tuple [value; v])) + in + Methods.add name value methods) + Methods.append elements (methods_of_xml el)) + (xml_node ~params ~children) + children) + +and xml_node ~params ~children = + Methods.from_list + [ + ( "xml_params", + Term.make + ~methods: + (Methods.from_list + (List.map (fun (k, v) -> (k, Term.make (`String v))) params)) + Term.unit ); + ( "xml_children", + Term.make (`Tuple (List.map (fun v -> term_of_xml v) children)) ); + ] + +and term_of_xml v = Term.make ~methods:(methods_of_xml v) `Null + +let _ = + Lang.add_builtin "_internal_xml_parser_" ~category:`String ~flags:[`Hidden] + ~descr:"Internal xml parser" + [ + ("type", Value.RuntimeType.t, None, Some "Runtime type"); + ("", Lang.string_t, None, None); + ] + (Lang.univ_t ()) + (fun p -> + let s = Lang.to_string (List.assoc "" p) in + let ty = Value.RuntimeType.of_value (List.assoc "type" p) in + let ty = Type.fresh ty in + try + let xml = Xml.parse_string s in + let tm = term_of_xml xml in + Typechecking.check ~throw:(fun exn -> raise exn) tm; + Typing.(ty <: tm.Term.t); + Evaluation.eval tm + with exn -> ( + let bt = Printexc.get_raw_backtrace () in + match exn with + | _ -> + Runtime_error.raise ~bt ~pos:(Lang.pos p) + ~message: + (Printf.sprintf + "Parse error: xml value cannot be parsed as type: %s" + (Type.to_string ty)) + "xml")) diff --git a/src/libs/extra/audioscrobbler.liq b/src/libs/extra/audioscrobbler.liq index 6888132945..f50e8b2c99 100644 --- a/src/libs/extra/audioscrobbler.liq +++ b/src/libs/extra/audioscrobbler.liq @@ -175,12 +175,12 @@ def audioscrobbler.api.track.updateNowPlaying( { lfm: { nowplaying: { - track: string.{ xml_params: {corrected: string} }, - artist: string.{ xml_params: {corrected: string} }, - album: string?.{ xml_params: {corrected: string} }, - albumArtist: string?.{ xml_params: {corrected: string} }, - timestamp: string, - ignoredMessage: {xml_params: {code: string}} + track: string.{ xml_params: {corrected: int} }, + artist: string.{ xml_params: {corrected: int} }, + album: string?.{ xml_params: {corrected: int} }, + albumArtist: string?.{ xml_params: {corrected: int} }, + timestamp: float, + ignoredMessage: {xml_params: {code: int}} }, xml_params: {status: string} } @@ -277,14 +277,14 @@ def audioscrobbler.api.track.scrobble( lfm: { scrobbles: { scrobble: { - track: string.{ xml_params: {corrected: string} }, - artist: string.{ xml_params: {corrected: string} }, - album: string?.{ xml_params: {corrected: string} }, - albumArtist: string?.{ xml_params: {corrected: string} }, - timestamp: string, - ignoredMessage: {xml_params: {code: string}} + track: string.{ xml_params: {corrected: int} }, + artist: string.{ xml_params: {corrected: int} }, + album: string?.{ xml_params: {corrected: int} }, + albumArtist: string?.{ xml_params: {corrected: int} }, + timestamp: float, + ignoredMessage: {xml_params: {code: int}} }, - xml_params: {ignored: string, accepted: string} + xml_params: {ignored: int, accepted: int} }, xml_params: {status: string} } From cf3f99e93f85822def44d95283e8dac7e3df5ea2 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Tue, 10 Dec 2024 01:12:32 -0600 Subject: [PATCH 05/11] Delete :Format --- :Format | 64 --------------------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 :Format diff --git a/:Format b/:Format deleted file mode 100644 index 807d125286..0000000000 --- a/:Format +++ /dev/null @@ -1,64 +0,0 @@ -let rec methods_of_xml = function - | Xml.PCData s -> ("text", Lang.string s) - | Xml.Element (name, params, ([Xml.PCData s] as children)) -> - (name, Lang.meth (Lang.string s) (xml_node ~params ~children)) - | Xml.Element (name, params, children) -> - ( name, - Lang.record ( - (List.fold_left - (fun methods el -> - let (name, v) = methods_of_xml el in - let v = - match Methods.find_opt name methods with - | None -> v - | Some (`Tuple {value}) -> Value.make (`Tuple (value @[v])) - | Some value -> (name, Value.make (`Tuple [value; v])) - in - Methods.add name value methods) - Methods.append elements (methods_of_xml el)) - (xml_node ~params ~children) - children) - -and xml_node ~params ~children = - Methods.from_list - [ - ( "xml_params", - Term.make - ~methods: - (Methods.from_list - (List.map (fun (k, v) -> (k, Term.make (`String v))) params)) - Term.unit ); - ( "xml_children", - Term.make (`Tuple (List.map (fun v -> term_of_xml v) children)) ); - ] - -and term_of_xml v = Term.make ~methods:(methods_of_xml v) `Null - -let _ = - Lang.add_builtin "_internal_xml_parser_" ~category:`String ~flags:[`Hidden] - ~descr:"Internal xml parser" - [ - ("type", Value.RuntimeType.t, None, Some "Runtime type"); - ("", Lang.string_t, None, None); - ] - (Lang.univ_t ()) - (fun p -> - let s = Lang.to_string (List.assoc "" p) in - let ty = Value.RuntimeType.of_value (List.assoc "type" p) in - let ty = Type.fresh ty in - try - let xml = Xml.parse_string s in - let tm = term_of_xml xml in - Typechecking.check ~throw:(fun exn -> raise exn) tm; - Typing.(ty <: tm.Term.t); - Evaluation.eval tm - with exn -> ( - let bt = Printexc.get_raw_backtrace () in - match exn with - | _ -> - Runtime_error.raise ~bt ~pos:(Lang.pos p) - ~message: - (Printf.sprintf - "Parse error: xml value cannot be parsed as type: %s" - (Type.to_string ty)) - "xml")) From 0bd5c9db85aec114efb0eec31b56c27d1c5b5033 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Wed, 11 Dec 2024 12:20:41 +0100 Subject: [PATCH 06/11] No timestamp. --- src/libs/extra/audioscrobbler.liq | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/extra/audioscrobbler.liq b/src/libs/extra/audioscrobbler.liq index f50e8b2c99..347d9bb00e 100644 --- a/src/libs/extra/audioscrobbler.liq +++ b/src/libs/extra/audioscrobbler.liq @@ -179,7 +179,6 @@ def audioscrobbler.api.track.updateNowPlaying( artist: string.{ xml_params: {corrected: int} }, album: string?.{ xml_params: {corrected: int} }, albumArtist: string?.{ xml_params: {corrected: int} }, - timestamp: float, ignoredMessage: {xml_params: {code: int}} }, xml_params: {status: string} From 496d2fa57fd264e3f1ea0a3a83616607f9d4c779 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Wed, 11 Dec 2024 12:29:54 +0100 Subject: [PATCH 07/11] Fix log. --- src/libs/extra/audioscrobbler.liq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/extra/audioscrobbler.liq b/src/libs/extra/audioscrobbler.liq index 347d9bb00e..a26c7b471c 100644 --- a/src/libs/extra/audioscrobbler.liq +++ b/src/libs/extra/audioscrobbler.liq @@ -292,7 +292,7 @@ def audioscrobbler.api.track.scrobble( log.info( label="audioscrobbler.api.track.scrobble", - "Done submitting updateNowPlaying with: #{params}" + "Done submitting scrobble with: #{params}" ) v From b0e4b5fd1d828ecd741f50cc3813e4210c023b0c Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Wed, 11 Dec 2024 12:41:48 +0100 Subject: [PATCH 08/11] Added CHANGES entry. --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index a47e28636c..989fc402c2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ New: wav dexcoder. - Added optional `buffer_size` parameter to `input.alsa` and `output.alsa` (#4243) +- Reimplemented audioscrobbler support natively using the more + recent protocol (#4250) Changed: From 1bb72f955ac71d05553c473947d10f3fb66038e9 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Fri, 13 Dec 2024 11:51:51 +0100 Subject: [PATCH 09/11] Add pre-processor. --- src/libs/extra/audioscrobbler.liq | 55 +++++++++++++++++++------------ 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/libs/extra/audioscrobbler.liq b/src/libs/extra/audioscrobbler.liq index a26c7b471c..bbeff2ad72 100644 --- a/src/libs/extra/audioscrobbler.liq +++ b/src/libs/extra/audioscrobbler.liq @@ -315,6 +315,7 @@ let source_on_metadata = source.on_metadata # @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set. # @param ~delay Submit song when there is only this delay left, in seconds. # @param ~force If remaining time is null, the song will be assumed to be skipped or cut, and not submitted. Set this to `true` to prevent this behavior +# @param ~metadata_preprocessor Metadata pre-processor callback. Can be used to change metadata on-the-fly before sending to nowPlaying/scrobble. If returning an empty metadata, nothing is sent at all. def audioscrobbler.submit( ~username, ~password, @@ -322,6 +323,7 @@ def audioscrobbler.submit( ~api_secret=null(), ~delay=10., ~force=false, + ~metadata_preprocessor=fun (m) -> m, s ) = session_key = @@ -336,30 +338,41 @@ def audioscrobbler.submit( def c(v) = v == "" ? null() : v end + m = metadata_preprocessor(m) track = m["title"] - album = c(m["album"]) artist = m["artist"] - trackNumber = - try - null.map(int_of_string, c(m["tracknumber"])) - catch _ do - null() - end - albumArtist = c(m["albumartist"]) - ignore( - fn( - username=username, - password=password, - api_key=api_key, - api_secret=api_secret, - session_key=session_key, - track=track, - artist=artist, - album=album, - trackNumber=trackNumber, - albumArtist=albumArtist + + if + track == "" or artist == "" + then + log.info( + label="audioscrobbler.submit", + "No artist or track present: metadata submission disabled!" ) - ) + else + album = c(m["album"]) + trackNumber = + try + null.map(int_of_string, c(m["tracknumber"])) + catch _ do + null() + end + albumArtist = c(m["albumartist"]) + ignore( + fn( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret, + session_key=session_key, + track=track, + artist=artist, + album=album, + trackNumber=trackNumber, + albumArtist=albumArtist + ) + ) + end end def now_playing(m) = From 22606e88f4bb0655868fbedfc47d816986efba5d Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Sat, 14 Dec 2024 13:15:30 +0100 Subject: [PATCH 10/11] Split functions, fix script --- doc/content/liq/radiopi.liq | 19 ++-- src/libs/extra/audioscrobbler.liq | 160 +++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 56 deletions(-) diff --git a/doc/content/liq/radiopi.liq b/doc/content/liq/radiopi.liq index 06553b29bd..9a2e9fb291 100644 --- a/doc/content/liq/radiopi.liq +++ b/doc/content/liq/radiopi.liq @@ -128,9 +128,9 @@ interlude = single("/home/radiopi/fallback.mp3") # Lastfm submission def lastfm(m) = if - (m["type"] == "chansons") - then - if + m["type"] == "chansons" + and + ( m["canal"] == "reggae" or @@ -138,12 +138,13 @@ def lastfm(m) = or m["canal"] == "That70Sound" ) - then - canal = - if (m["canal"] == "That70Sound") then "70sound" else m["canal"] end - user = "radiopi-" ^ canal - lastfm.submit(user=user, password="xXXxx", m) - end + + then + canal = if (m["canal"] == "That70Sound") then "70sound" else m["canal"] end + username = "radiopi-" ^ canal + audioscrobbler.api.track.scrobble.metadata( + username=username, password="xXXxx", m + ) end end diff --git a/src/libs/extra/audioscrobbler.liq b/src/libs/extra/audioscrobbler.liq index bbeff2ad72..48b339185a 100644 --- a/src/libs/extra/audioscrobbler.liq +++ b/src/libs/extra/audioscrobbler.liq @@ -200,6 +200,79 @@ def audioscrobbler.api.track.updateNowPlaying( end end +# @flag hidden +def audioscrobbler.api.apply_meta( + ~name, + ~username, + ~password, + ~api_key, + ~api_secret, + ~session_key, + fn, + m +) = + def c(v) = + v == "" ? null() : v + end + track = m["title"] + artist = m["artist"] + + if + track == "" or artist == "" + then + log.info( + label=name, + "No artist or track present: metadata submission disabled!" + ) + else + album = c(m["album"]) + trackNumber = + try + null.map(int_of_string, c(m["tracknumber"])) + catch _ do + null() + end + albumArtist = c(m["albumartist"]) + ignore( + fn( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret, + session_key=session_key, + track=track, + artist=artist, + album=album, + trackNumber=trackNumber, + albumArtist=albumArtist + ) + ) + end +end + +# Submit a track using its metadata to the audioscrobbler +# `track.updateNowPlaying` API. +# @category Interaction +def audioscrobbler.api.track.updateNowPlaying.metadata( + ~username, + ~password, + ~session_key=null(), + ~api_key=null(), + ~api_secret=null(), + m +) = + audioscrobbler.api.apply_meta( + username=username, + password=password, + session_key=session_key, + api_key=api_key, + api_secret=api_secret, + name="audioscrobbler.api.track.updateNowPlaying", + audioscrobbler.api.track.updateNowPlaying, + m + ) +end + # Submit a track to the audioscrobbler # `track.scrobble` API. # @category Interaction @@ -304,8 +377,28 @@ def audioscrobbler.api.track.scrobble( end end -let source_on_end = source.on_end -let source_on_metadata = source.on_metadata +# Submit a track to the audioscrobbler +# `track.scrobble` API using its metadata. +# @category Interaction +def audioscrobbler.api.track.scrobble.metadata( + ~username, + ~password, + ~session_key=null(), + ~api_key=null(), + ~api_secret=null(), + m +) = + audioscrobbler.api.apply_meta( + username=username, + password=password, + session_key=session_key, + api_key=api_key, + api_secret=api_secret, + name="audioscrobbler.api.track.scrobble", + audioscrobbler.api.track.scrobble, + m + ) +end # Submit songs using audioscrobbler, respecting the full protocol: # First signal song as now playing when starting, and @@ -334,50 +427,16 @@ def audioscrobbler.submit( api_secret=api_secret ) - def apply_meta(fn, m) = - def c(v) = - v == "" ? null() : v - end - m = metadata_preprocessor(m) - track = m["title"] - artist = m["artist"] - - if - track == "" or artist == "" - then - log.info( - label="audioscrobbler.submit", - "No artist or track present: metadata submission disabled!" - ) - else - album = c(m["album"]) - trackNumber = - try - null.map(int_of_string, c(m["tracknumber"])) - catch _ do - null() - end - albumArtist = c(m["albumartist"]) - ignore( - fn( - username=username, - password=password, - api_key=api_key, - api_secret=api_secret, - session_key=session_key, - track=track, - artist=artist, - album=album, - trackNumber=trackNumber, - albumArtist=albumArtist - ) - ) - end - end - def now_playing(m) = try - apply_meta(audioscrobbler.api.track.updateNowPlaying, m) + audioscrobbler.api.track.updateNowPlaying.metadata( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret, + session_key=session_key, + metadata_preprocessor(m) + ) catch err do log.important( "Error while submitting nowplaying info for #{source.id(s)}: #{err}" @@ -385,7 +444,7 @@ def audioscrobbler.submit( end end - s = source_on_metadata(s, now_playing) + s = source.on_metadata(s, now_playing) f = fun (rem, m) -> # Avoid skipped songs @@ -393,7 +452,14 @@ def audioscrobbler.submit( rem > 0. or force then try - apply_meta(audioscrobbler.api.track.scrobble, m) + audioscrobbler.api.track.scrobble.metadata( + username=username, + password=password, + api_key=api_key, + api_secret=api_secret, + session_key=session_key, + metadata_preprocessor(m) + ) catch err do log.important( "Error while submitting scrobble info for #{source.id(s)}: #{err}" @@ -406,5 +472,5 @@ def audioscrobbler.submit( "Remaining time null: will not submit song (song skipped ?)" ) end - source_on_end(s, delay=delay, f) + source.on_end(s, delay=delay, f) end From 1f17cc2da5e31c441dbdfe57f9ae66e9857a0802 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Sat, 14 Dec 2024 13:41:20 +0100 Subject: [PATCH 11/11] Fix formatting. --- src/libs/http.liq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/http.liq b/src/libs/http.liq index c91afd3f74..70d550219a 100644 --- a/src/libs/http.liq +++ b/src/libs/http.liq @@ -981,7 +981,7 @@ def http.headers.content_disposition(headers) = type: string, filename?: string, name?: string, - args: [(string*string?)] + args: [(string * string?)] } ) end,