PageRenderTime 33ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/config/awesome/daemons/playerctl.lua

https://github.com/Mange/dotfiles
Lua | 174 lines | 132 code | 28 blank | 14 comment | 10 complexity | a165eaa135d3fb89f1e848b70525ae99 MD5 | raw file
  1. local spawn = require("awful.spawn")
  2. local timer = require("gears.timer")
  3. local utils = require("utils")
  4. local function repair_spotify_broken_art_url(url)
  5. if url then
  6. return string.gsub(url, "https://open.spotify.com/image/", "https://i.scdn.co/image/")
  7. else
  8. return url
  9. end
  10. end
  11. -- Index metadata on {player -> {key -> value}}
  12. -- TODO: Implement a LRU cache here to limit to max 3 players at a time
  13. local all_metadata = {}
  14. -- Index status as {player -> status}
  15. -- TODO: Implement a LRU cache here to limit to max 3 players at a time
  16. local all_statuses = {}
  17. local playerctl = {
  18. current_player = nil,
  19. current_status = nil,
  20. current_metadata = {},
  21. metadata = all_metadata,
  22. statuses = all_statuses,
  23. }
  24. local metadata_map = {
  25. ["mpris:trackid"] = "track_id",
  26. ["mpris:length"] = "length",
  27. ["mpris:artUrl"] = "art_url",
  28. ["xesam:album"] = "album_name",
  29. ["xesam:albumArtist"] = "album_artist",
  30. ["xesam:artist"] = "artist",
  31. ["xesam:autoRating"] = "auto_rating",
  32. ["xesam:discNumber"] = "disc_number",
  33. ["xesam:title"] = "title",
  34. ["xesam:trackNumber"] = "track_number",
  35. ["xesam:url"] = "url",
  36. }
  37. local function metadata_get_all(playername)
  38. local data = all_metadata[playername]
  39. if (data == nil) then
  40. data = {}
  41. all_metadata[playername] = data
  42. end
  43. return data
  44. end
  45. -- Update metadata for a player
  46. local function metadata_set(playername, metadata_name, value)
  47. local data = metadata_get_all(playername)
  48. local key = metadata_map[metadata_name] or metadata_name
  49. if key == "art_url" then
  50. value = repair_spotify_broken_art_url(value)
  51. end
  52. data[key] = value
  53. end
  54. local function set_current_player(playername)
  55. playerctl.current_player = playername
  56. playerctl.current_status = all_statuses[playername] or "stopped"
  57. playerctl.current_metadata = metadata_get_all(playername)
  58. end
  59. local function status_set(playername, status)
  60. local old_status = all_statuses[playername]
  61. all_statuses[playername] = status
  62. if playerctl.current_player == playername then
  63. playerctl.current_status = status
  64. elseif status == "playing" then
  65. set_current_player(playername)
  66. end
  67. if old_status ~= status then
  68. awesome.emit_signal("mange:playerctl:update", playerctl:current())
  69. end
  70. end
  71. -- Since a lot of metadata will update almost at the same time, debounce
  72. -- sending out the event until it has quieted down a bit again.
  73. local metadata_debounce = timer {
  74. timeout = 0.2,
  75. call_now = false,
  76. autostart = false,
  77. single_shot = true,
  78. callback = function()
  79. awesome.emit_signal("mange:playerctl:update", playerctl:current())
  80. end
  81. }
  82. -- playerctl outputs this each time a metadata key changes:
  83. -- <playername> <metadataname> <value>
  84. -- There is variable whitespace between metadata name and the value.
  85. local function handle_metadata_change(line)
  86. local playername, metadataname, value = string.match(line, "(%S+)%s+(%S+)%s+(.+)")
  87. metadata_set(playername, metadataname, value)
  88. -- Since this player just changed, make it the current player
  89. set_current_player(playername)
  90. metadata_debounce:again()
  91. end
  92. -- playerctl outputs this each time status changes:
  93. -- <playername> <status>
  94. -- The whitespace is a tab character
  95. local function handle_status_change(line)
  96. local playername, status = string.match(line, "([^\t]+)\t([^\t]+)")
  97. status_set(playername, status)
  98. end
  99. local function spawn_status_watcher()
  100. return spawn.with_line_callback(
  101. {
  102. "playerctl", "status",
  103. "--no-messages", "--all-players",
  104. "--follow", "--format", "{{playerName}}\t{{lc(status)}}"
  105. },
  106. {stdout = handle_status_change}
  107. )
  108. end
  109. local function spawn_metadata_watcher()
  110. return spawn.with_line_callback(
  111. {"playerctl", "metadata", "--no-messages", "--all-players", "--follow"},
  112. {stdout = handle_metadata_change}
  113. )
  114. end
  115. function playerctl:current()
  116. return {
  117. playername = self.current_player,
  118. status = self.current_status,
  119. metadata = self.current_metadata,
  120. }
  121. end
  122. function playerctl:on_update(func)
  123. awesome.connect_signal("mange:playerctl:update", func)
  124. end
  125. local function spawn_playerctl(command)
  126. local cmdline = {"playerctl"}
  127. local player = playerctl.current_player
  128. if player then
  129. table.insert(cmdline, "-p")
  130. table.insert(cmdline, player)
  131. end
  132. table.insert(cmdline, command)
  133. spawn(cmdline)
  134. end
  135. function playerctl:play_pause()
  136. spawn_playerctl("play-pause")
  137. end
  138. function playerctl:previous()
  139. spawn_playerctl("previous")
  140. end
  141. function playerctl:next()
  142. spawn_playerctl("next")
  143. end
  144. utils.kill_on_exit(spawn_status_watcher())
  145. utils.kill_on_exit(spawn_metadata_watcher())
  146. return playerctl