awesompd.lua 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229
  1. ---------------------------------------------------------------------------
  2. -- @author Alexander Yakushev <yakushev.alex@gmail.com>
  3. -- @copyright 2010-2013 Alexander Yakushev
  4. -- @release v1.2.4
  5. ---------------------------------------------------------------------------
  6. local wibox = require("wibox")
  7. local awful = require('awful')
  8. local beautiful = require('beautiful')
  9. local naughty = require('naughty')
  10. local format = string.format
  11. local module_path = (...):match ("(.+/)[^/]+$") or ""
  12. local awesompd = {}
  13. -- Function for checking icons and modules. Checks if a file exists,
  14. -- and if it does, returns the path to file, nil otherwise.
  15. function awesompd.try_load(file)
  16. if awful.util.file_readable(file) then
  17. return file
  18. end
  19. end
  20. -- Function for loading modules.
  21. function awesompd.try_require(module)
  22. if awesompd.try_load(awful.util.getdir("config") .. '/'..
  23. module_path .. module .. ".lua") then
  24. return require(module_path .. module)
  25. else
  26. return require(module)
  27. end
  28. end
  29. local utf8 = awesompd.try_require("utf8")
  30. asyncshell = awesompd.try_require("asyncshell")
  31. local jamendo = awesompd.try_require("jamendo")
  32. -- Constants
  33. awesompd.PLAYING = "Playing"
  34. awesompd.PAUSED = "Paused"
  35. awesompd.STOPPED = "MPD stopped"
  36. awesompd.DISCONNECTED = "Disconnected"
  37. awesompd.MOUSE_LEFT = 1
  38. awesompd.MOUSE_MIDDLE = 2
  39. awesompd.MOUSE_RIGHT = 3
  40. awesompd.MOUSE_SCROLL_UP = 4
  41. awesompd.MOUSE_SCROLL_DOWN = 5
  42. awesompd.NOTIFY_VOLUME = 1
  43. awesompd.NOTIFY_REPEAT = 2
  44. awesompd.NOTIFY_RANDOM = 3
  45. awesompd.NOTIFY_SINGLE = 4
  46. awesompd.NOTIFY_CONSUME = 5
  47. awesompd.FORMAT_MP3 = jamendo.FORMAT_MP3
  48. awesompd.FORMAT_OGG = jamendo.FORMAT_OGG
  49. awesompd.ESCAPE_SYMBOL_MAPPING = {}
  50. awesompd.ESCAPE_SYMBOL_MAPPING["&"] = "&amp;"
  51. -- Menus do not handle symbol escaping correctly, so they need their
  52. -- own mapping.
  53. awesompd.ESCAPE_MENU_SYMBOL_MAPPING = {}
  54. awesompd.ESCAPE_MENU_SYMBOL_MAPPING["&"] = "'n'"
  55. -- /// Current track variables and functions ///
  56. -- Returns a string for the given track to be displayed in the widget
  57. -- and notification.
  58. function awesompd.get_display_name(track)
  59. if track.display_name then
  60. return track.display_name
  61. elseif track.artist_name and track.track_name then
  62. return track.artist_name .. " - " .. track.name
  63. end
  64. end
  65. -- Returns a track display name, album name (if exists) and album
  66. -- release year (if exists).
  67. function awesompd.get_extended_info(track)
  68. local result = awesompd.get_display_name(track)
  69. if track.album_name then
  70. result = result .. "\n" .. track.album_name
  71. end
  72. if track.year then
  73. result = result .. "\n" .. track.year
  74. end
  75. return result
  76. end
  77. -- Returns true if the current status is either PLAYING or PAUSED
  78. function awesompd:playing_or_paused()
  79. return self.status == awesompd.PLAYING
  80. or self.status == awesompd.PAUSED
  81. end
  82. -- /// Helper functions ///
  83. -- Just like awful.util.pread, but takes an argument how to read like
  84. -- "*line" or "*all".
  85. function awesompd.pread(com, mode)
  86. local f = io.popen(com, 'r')
  87. local result = nil
  88. if f then
  89. result = f:read(mode)
  90. f:close()
  91. end
  92. return result
  93. end
  94. -- Slightly modified function awful.util.table.join.
  95. function awesompd.ajoin(buttons)
  96. local result = {}
  97. for i = 1, #buttons do
  98. if buttons[i] then
  99. for k, v in pairs(buttons[i]) do
  100. if type(k) == "number" then
  101. table.insert(result, v)
  102. else
  103. result[k] = v
  104. end
  105. end
  106. end
  107. end
  108. return result
  109. end
  110. -- Splits a given string with linebreaks into an array.
  111. function awesompd.split(s)
  112. local l = { n = 0 }
  113. if s == "" then
  114. return l
  115. end
  116. s = s .. "\n"
  117. local f = function (s)
  118. l.n = l.n + 1
  119. l[l.n] = s
  120. end
  121. local p = "%s*(.-)%s*\n%s*"
  122. s = string.gsub(s,p,f)
  123. return l
  124. end
  125. -- Returns the given string if it is not nil or non-empty, otherwise
  126. -- returns nil.
  127. local function non_empty(s)
  128. if s and s ~= "" then
  129. return s
  130. end
  131. end
  132. -- Icons
  133. function awesompd.load_icons(path)
  134. awesompd.ICONS = {}
  135. awesompd.ICONS.PLAY = awesompd.try_load(path .. "/play_icon.png")
  136. awesompd.ICONS.PAUSE = awesompd.try_load(path .. "/pause_icon.png")
  137. awesompd.ICONS.PLAY_PAUSE = awesompd.try_load(path .. "/play_pause_icon.png")
  138. awesompd.ICONS.STOP = awesompd.try_load(path .. "/stop_icon.png")
  139. awesompd.ICONS.NEXT = awesompd.try_load(path .. "/next_icon.png")
  140. awesompd.ICONS.PREV = awesompd.try_load(path .. "/prev_icon.png")
  141. awesompd.ICONS.CHECK = awesompd.try_load(path .. "/check_icon.png")
  142. awesompd.ICONS.RADIO = awesompd.try_load(path .. "/radio_icon.png")
  143. awesompd.ICONS.DEFAULT_ALBUM_COVER =
  144. awesompd.try_load(path .. "/default_album_cover.png")
  145. end
  146. -- Function that returns a new awesompd object.
  147. function awesompd:create()
  148. -- Initialization
  149. local instance = {}
  150. setmetatable(instance,self)
  151. self.__index = self
  152. instance.current_server = 1
  153. instance.widget = wibox.layout.fixed.horizontal()
  154. instance.notification = nil
  155. instance.scroll_pos = 1
  156. instance.text = ""
  157. instance.to_notify = false
  158. instance.album_cover = nil
  159. instance.current_track = { }
  160. instance.recreate_menu = true
  161. instance.recreate_playback = true
  162. instance.recreate_list = true
  163. instance.recreate_servers = true
  164. instance.recreate_options = true
  165. instance.recreate_jamendo_formats = true
  166. instance.recreate_jamendo_order = true
  167. instance.recreate_jamendo_browse = true
  168. instance.current_number = 0
  169. instance.menu_shown = false
  170. instance.state_volume = "NaN"
  171. instance.state_repeat = "NaN"
  172. instance.state_random = "NaN"
  173. instance.state_single = "NaN"
  174. instance.state_consume = "NaN"
  175. -- Default user options
  176. instance.servers = { { server = "localhost", port = 6600 } }
  177. instance.font = "Monospace"
  178. instance.font_color = beautiful.fg_normal
  179. instance.background = beautiful.bg_normal
  180. instance.scrolling = true
  181. instance.output_size = 30
  182. instance.update_interval = 10
  183. instance.path_to_icons = ""
  184. instance.ldecorator = " "
  185. instance.rdecorator = " "
  186. instance.jamendo_format = awesompd.FORMAT_MP3
  187. instance.show_album_cover = true
  188. instance.album_cover_size = 50
  189. instance.browser = "firefox"
  190. -- Widget configuration
  191. instance.widget:connect_signal("mouse::enter", function(c)
  192. instance:notify_track()
  193. end)
  194. instance.widget:connect_signal("mouse::leave", function(c)
  195. instance:hide_notification()
  196. end)
  197. return instance
  198. end
  199. -- Registers timers for the widget
  200. function awesompd:run()
  201. self.load_icons(self.path_to_icons)
  202. jamendo.set_current_format(self.jamendo_format)
  203. if self.album_cover_size > 100 then
  204. self.album_cover_size = 100
  205. end
  206. self.text_widget = wibox.widget.textbox()
  207. if self.widget_icon then
  208. self.icon_widget = wibox.widget.imagebox()
  209. self.icon_widget:set_image(self.widget_icon)
  210. self.widget:add(self.icon_widget)
  211. end
  212. self.widget:add(self.text_widget)
  213. self:update_track()
  214. self:check_playlists()
  215. if scheduler then
  216. scheduler.register_recurring("awesompd_scroll", 1,
  217. function() self:update_widget() end)
  218. scheduler.register_recurring("awesompd_update", self.update_interval,
  219. function() self:update_track() end)
  220. else
  221. self.update_widget_timer = timer({ timeout = 1 })
  222. self.update_widget_timer:connect_signal("timeout", function()
  223. self:update_widget()
  224. end)
  225. self.update_widget_timer:start()
  226. self.update_track_timer = timer({ timeout = self.update_interval })
  227. self.update_track_timer:connect_signal("timeout", function()
  228. self:update_track()
  229. end)
  230. self.update_track_timer:start()
  231. end
  232. end
  233. -- Function that registers buttons on the widget.
  234. function awesompd:register_buttons(buttons)
  235. widget_buttons = {}
  236. self.global_bindings = {}
  237. for b=1, #buttons do
  238. if type(buttons[b][1]) == "string" then
  239. mods = { buttons[b][1] }
  240. else
  241. mods = buttons[b][1]
  242. end
  243. if type(buttons[b][2]) == "number" then
  244. -- This is a mousebinding, bind it to the widget
  245. table.insert(widget_buttons,
  246. awful.button(mods, buttons[b][2], buttons[b][3]))
  247. else
  248. -- This is a global keybinding, remember it for later usage in append_global_keys
  249. table.insert(self.global_bindings, awful.key(mods, buttons[b][2], buttons[b][3]))
  250. end
  251. end
  252. self.widget:buttons(self.ajoin(widget_buttons))
  253. end
  254. -- Takes the current table with keybindings and adds widget's own
  255. -- global keybindings that were specified in register_buttons.
  256. -- If keytable is not specified, then adds bindings to default
  257. -- globalkeys table. If specified, then adds bindings to keytable and
  258. -- returns it.
  259. function awesompd:append_global_keys(keytable)
  260. if keytable then
  261. for i = 1, #self.global_bindings do
  262. keytable = awful.util.table.join(keytable, self.global_bindings[i])
  263. end
  264. return keytable
  265. else
  266. for i = 1, #self.global_bindings do
  267. globalkeys = awful.util.table.join(globalkeys, self.global_bindings[i])
  268. end
  269. end
  270. end
  271. -- /// Group of mpc command functions ///
  272. -- Returns a mpc command with all necessary parameters. Boolean
  273. -- human_readable argument configures if the command special
  274. -- formatting of the output (to be later used in parsing) should not
  275. -- be used.
  276. function awesompd:mpcquery(human_readable)
  277. local result =
  278. "mpc -h " .. self.servers[self.current_server].server ..
  279. " -p " .. self.servers[self.current_server].port .. " "
  280. if human_readable then
  281. return result
  282. else
  283. return result ..' -f "%file%-<>-%name%-<>-%title%-<>-%artist%-<>-%album%" '
  284. end
  285. end
  286. -- Takes a command to mpc and a hook that is provided with awesompd
  287. -- instance and the result of command execution.
  288. function awesompd:command(com,hook)
  289. local file = io.popen(self:mpcquery() .. com)
  290. if hook then
  291. hook(self,file)
  292. end
  293. file:close()
  294. end
  295. -- Takes a command to mpc and read mode and returns the result.
  296. function awesompd:command_read(com, mode)
  297. mode = mode or "*line"
  298. self:command(com, function(_, f)
  299. result = f:read(mode)
  300. end)
  301. return result
  302. end
  303. function awesompd:command_playpause()
  304. return function()
  305. self:command("toggle",self.update_track)
  306. end
  307. end
  308. function awesompd:command_next_track()
  309. return function()
  310. self:command("next",self.update_track)
  311. end
  312. end
  313. function awesompd:command_prev_track()
  314. return function()
  315. self:command("seek 0")
  316. self:command("prev",self.update_track)
  317. end
  318. end
  319. function awesompd:command_stop()
  320. return function()
  321. self:command("stop",self.update_track)
  322. end
  323. end
  324. function awesompd:command_play_specific(n)
  325. return function()
  326. self:command("play " .. n,self.update_track)
  327. end
  328. end
  329. function awesompd:command_volume_up()
  330. return function()
  331. self:command("volume +5")
  332. self:update_track() -- Nasty! I should replace it with proper callback later.
  333. self:notify_state(self.NOTIFY_VOLUME)
  334. end
  335. end
  336. function awesompd:command_volume_down()
  337. return function()
  338. self:command("volume -5")
  339. self:update_track()
  340. self:notify_state(self.NOTIFY_VOLUME)
  341. end
  342. end
  343. function awesompd:command_load_playlist(name)
  344. return function()
  345. self:command("load \"" .. name .. "\"", function()
  346. self.recreate_menu = true
  347. end)
  348. end
  349. end
  350. function awesompd:command_replace_playlist(name)
  351. return function()
  352. self:command("clear")
  353. self:command("load \"" .. name .. "\"")
  354. self:command("play 1", self.update_track)
  355. end
  356. end
  357. function awesompd:command_clear_playlist()
  358. return function()
  359. self:command("clear", self.update_track)
  360. self.recreate_list = true
  361. self.recreate_menu = true
  362. end
  363. end
  364. function awesompd:command_open_in_browser(link)
  365. return function()
  366. if self.browser then
  367. awful.util.spawn(self.browser .. " '" .. link .. "'")
  368. end
  369. end
  370. end
  371. --- Change to the previous server.
  372. function awesompd:command_previous_server()
  373. return function()
  374. servers = table.getn(self.servers)
  375. if servers == 1 or servers == nil then
  376. return
  377. else
  378. if self.current_server > 1 then
  379. self:change_server(self.current_server - 1)
  380. else
  381. self:change_server(servers)
  382. end
  383. end
  384. end
  385. end
  386. --- Change to the previous server.
  387. function awesompd:command_next_server()
  388. return function()
  389. servers = table.getn(self.servers)
  390. if servers == 1 or servers == nil then
  391. return
  392. else
  393. if self.current_server < servers then
  394. self:change_server(self.current_server + 1)
  395. else
  396. self:change_server(1)
  397. end
  398. end
  399. end
  400. end
  401. -- /// End of mpc command functions ///
  402. -- /// Menu generation functions ///
  403. function awesompd:command_show_menu()
  404. return
  405. function()
  406. self:hide_notification()
  407. if self.recreate_menu then
  408. local new_menu = {}
  409. if self.main_menu ~= nil then
  410. self.main_menu:hide()
  411. end
  412. if self.status ~= awesompd.DISCONNECTED
  413. then
  414. self:check_list()
  415. self:check_playlists()
  416. local jamendo_menu = { { "Search by",
  417. { { "Nothing (Top 100)", self:menu_jamendo_top() },
  418. { "Artist", self:menu_jamendo_search_by(jamendo.SEARCH_ARTIST) },
  419. { "Album", self:menu_jamendo_search_by(jamendo.SEARCH_ALBUM) },
  420. { "Tag", self:menu_jamendo_search_by(jamendo.SEARCH_TAG) }}} }
  421. local browse_menu = self:menu_jamendo_browse()
  422. if browse_menu then
  423. table.insert(jamendo_menu, browse_menu)
  424. end
  425. table.insert(jamendo_menu, self:menu_jamendo_format())
  426. table.insert(jamendo_menu, self:menu_jamendo_order())
  427. new_menu = { { "Playback", self:menu_playback() },
  428. { "Options", self:menu_options() },
  429. { "List", self:menu_list() },
  430. { "Playlists", self:menu_playlists() },
  431. { "Jamendo", jamendo_menu } }
  432. end
  433. table.insert(new_menu, { "Servers", self:menu_servers() })
  434. self.main_menu = awful.menu({ items = new_menu, theme = { width = 300 } })
  435. self.recreate_menu = false
  436. end
  437. self.main_menu:toggle()
  438. end
  439. end
  440. -- Returns an icon for a checkbox menu item if it is checked, nil
  441. -- otherwise.
  442. function awesompd:menu_item_toggle(checked)
  443. return checked and self.ICONS.CHECK or nil
  444. end
  445. -- Returns an icon for a radiobox menu item if it is selected, nil
  446. -- otherwise.
  447. function awesompd:menu_item_radio(selected)
  448. return selected and self.ICONS.RADIO or nil
  449. end
  450. -- Returns the playback menu. Menu contains of:
  451. -- Play\Pause - always
  452. -- Previous - if the current track is not the first
  453. -- in the list and playback is not stopped
  454. -- Next - if the current track is not the last
  455. -- in the list and playback is not stopped
  456. -- Stop - if the playback is not stopped
  457. -- Clear playlist - always
  458. function awesompd:menu_playback()
  459. if self.recreate_playback then
  460. local new_menu = {}
  461. table.insert(new_menu, { "Play\\Pause",
  462. self:command_toggle(),
  463. self.ICONS.PLAY_PAUSE })
  464. if self:playing_or_paused() then
  465. if self.list_array and self.list_array[self.current_number-1] then
  466. table.insert(new_menu,
  467. { "Prev: " ..
  468. awesompd.protect_string(jamendo.replace_link(
  469. self.list_array[self.current_number - 1]),
  470. true),
  471. self:command_prev_track(), self.ICONS.PREV })
  472. end
  473. if self.list_array and self.current_number ~= #self.list_array then
  474. table.insert(new_menu,
  475. { "Next: " ..
  476. awesompd.protect_string(jamendo.replace_link(
  477. self.list_array[self.current_number + 1]),
  478. true),
  479. self:command_next_track(), self.ICONS.NEXT })
  480. end
  481. table.insert(new_menu, { "Stop", self:command_stop(), self.ICONS.STOP })
  482. table.insert(new_menu, { "", nil })
  483. end
  484. table.insert(new_menu, { "Clear playlist", self:command_clear_playlist() })
  485. self.recreate_playback = false
  486. playback_menu = new_menu
  487. end
  488. return playback_menu
  489. end
  490. -- Returns the current playlist menu. Menu consists of all elements in the playlist.
  491. function awesompd:menu_list()
  492. if self.recreate_list then
  493. local new_menu = {}
  494. if self.list_array then
  495. local total_count = #self.list_array
  496. local start_num = (self.current_number - 15 > 0) and self.current_number - 15 or 1
  497. local end_num = (self.current_number + 15 < total_count ) and self.current_number + 15 or total_count
  498. for i = start_num, end_num do
  499. table.insert(new_menu, { jamendo.replace_link(self.list_array[i]),
  500. self:command_play_specific(i),
  501. self.current_number == i and
  502. (self.status == self.PLAYING and self.ICONS.PLAY or self.ICONS.PAUSE)
  503. or nil} )
  504. end
  505. end
  506. self.recreate_list = false
  507. self.list_menu = new_menu
  508. end
  509. return self.list_menu
  510. end
  511. -- Returns the playlists menu. Menu consists of all files in the playlist folder.
  512. function awesompd:menu_playlists()
  513. if self.recreate_playlists then
  514. local new_menu = {}
  515. if #self.playlists_array > 0 then
  516. for i = 1, #self.playlists_array do
  517. local submenu = {}
  518. submenu[1] = { "Add to current", self:command_load_playlist(self.playlists_array[i]) }
  519. submenu[2] = { "Replace current", self:command_replace_playlist(self.playlists_array[i]) }
  520. new_menu[i] = { self.playlists_array[i], submenu }
  521. end
  522. table.insert(new_menu, {"", ""}) -- This is a separator
  523. end
  524. table.insert(new_menu, { "Refresh", function() self:check_playlists() end })
  525. self.recreate_playlists = false
  526. self.playlists_menu = new_menu
  527. end
  528. return self.playlists_menu
  529. end
  530. -- Returns the server menu. Menu consists of all servers specified by user during initialization.
  531. function awesompd:menu_servers()
  532. if self.recreate_servers then
  533. local new_menu = {}
  534. for i = 1, #self.servers do
  535. table.insert(new_menu, {"Server: " .. self.servers[i].server ..
  536. ", port: " .. self.servers[i].port,
  537. function() self:change_server(i) end,
  538. self:menu_item_radio(i == self.current_server)})
  539. end
  540. self.servers_menu = new_menu
  541. end
  542. return self.servers_menu
  543. end
  544. -- Returns the options menu. Menu works like checkboxes for it's elements.
  545. function awesompd:menu_options()
  546. if self.recreate_options then
  547. local new_menu = { { "Repeat", self:menu_toggle_repeat(),
  548. self:menu_item_toggle(self.state_repeat == "on")},
  549. { "Random", self:menu_toggle_random(),
  550. self:menu_item_toggle(self.state_random == "on")},
  551. { "Single", self:menu_toggle_single(),
  552. self:menu_item_toggle(self.state_single == "on")},
  553. { "Consume", self:menu_toggle_consume(),
  554. self:menu_item_toggle(self.state_consume == "on")} }
  555. self.options_menu = new_menu
  556. self.recreate_options = false
  557. end
  558. return self.options_menu
  559. end
  560. function awesompd:menu_toggle_random()
  561. return function()
  562. self:command("random",self.update_track)
  563. self:notify_state(self.NOTIFY_RANDOM)
  564. end
  565. end
  566. function awesompd:menu_toggle_repeat()
  567. return function()
  568. self:command("repeat",self.update_track)
  569. self:notify_state(self.NOTIFY_REPEAT)
  570. end
  571. end
  572. function awesompd:menu_toggle_single()
  573. return function()
  574. self:command("single",self.update_track)
  575. self:notify_state(self.NOTIFY_SINGLE)
  576. end
  577. end
  578. function awesompd:menu_toggle_consume()
  579. return function()
  580. self:command("consume",self.update_track)
  581. self:notify_state(self.NOTIFY_CONSUME)
  582. end
  583. end
  584. function awesompd:menu_jamendo_top()
  585. return
  586. function ()
  587. local track_table = jamendo.return_track_table()
  588. if not track_table then
  589. self:show_notification("Can't connect to Jamendo server", "Please check your network connection")
  590. else
  591. self:add_jamendo_tracks(track_table)
  592. self:show_notification("Jamendo Top 100 by " ..
  593. jamendo.current_request_table.params.order.short_display,
  594. format("Added %s tracks to the playlist",
  595. #track_table))
  596. end
  597. end
  598. end
  599. function awesompd:menu_jamendo_format()
  600. if self.recreate_jamendo_formats then
  601. local setformat =
  602. function(format)
  603. return function()
  604. jamendo.set_current_format(format)
  605. self.recreate_menu = true
  606. self.recreate_jamendo_formats = true
  607. end
  608. end
  609. local iscurr =
  610. function(f)
  611. return jamendo.current_request_table.params.streamencoding.value
  612. == f.value
  613. end
  614. local new_menu = {}
  615. for _, format in pairs(jamendo.ALL_FORMATS) do
  616. table.insert(new_menu, { format.display, setformat(format),
  617. self:menu_item_radio(iscurr(format))})
  618. end
  619. self.recreate_jamendo_formats = false
  620. self.jamendo_formats_menu = {
  621. "Format: " ..
  622. jamendo.current_request_table.params.streamencoding.short_display,
  623. new_menu }
  624. end
  625. return self.jamendo_formats_menu
  626. end
  627. function awesompd:menu_jamendo_browse()
  628. if self.recreate_jamendo_browse and self.browser
  629. and self.current_track.unique_name then
  630. local track = jamendo.get_track_by_link(self.current_track.unique_name)
  631. local new_menu
  632. if track then
  633. local artist_link =
  634. "http://www.jamendo.com/artist/" .. track.artist_link_name
  635. local album_link =
  636. "http://www.jamendo.com/album/" .. track.album_id
  637. new_menu = { { "Artist's page" ,
  638. self:command_open_in_browser(artist_link) },
  639. { "Album's page" ,
  640. self:command_open_in_browser(album_link) } }
  641. self.jamendo_browse_menu = { "Browse on Jamendo", new_menu }
  642. else
  643. self.jamendo_browse_menu = nil
  644. end
  645. end
  646. return self.jamendo_browse_menu
  647. end
  648. function awesompd:menu_jamendo_order()
  649. if self.recreate_jamendo_order then
  650. local setorder =
  651. function(order)
  652. return function()
  653. jamendo.set_current_order(order)
  654. self.recreate_menu = true
  655. self.recreate_jamendo_order = true
  656. end
  657. end
  658. local iscurr =
  659. function(o)
  660. return jamendo.current_request_table.params.order.value
  661. == o.value
  662. end
  663. local new_menu = {}
  664. for _, order in pairs(jamendo.ALL_ORDERS) do
  665. table.insert(new_menu, { order.display, setorder(order),
  666. self:menu_item_radio(iscurr(order))})
  667. end
  668. self.recreate_jamendo_order = false
  669. self.jamendo_order_menu = {
  670. "Order: " ..
  671. jamendo.current_request_table.params.order.short_display,
  672. new_menu }
  673. end
  674. return self.jamendo_order_menu
  675. end
  676. function awesompd:menu_jamendo_search_by(what)
  677. return function()
  678. local callback =
  679. function(s)
  680. local result = jamendo.search_by(what, s)
  681. if result then
  682. local track_count = #result.tracks
  683. self:add_jamendo_tracks(result.tracks)
  684. self:show_notification(format("%s \"%s\" was found",
  685. what.display,
  686. result.search_res.name),
  687. format("Added %s tracks to the playlist",
  688. track_count))
  689. else
  690. self:show_notification("Search failed",
  691. format("%s \"%s\" was not found",
  692. what.display, s))
  693. end
  694. end
  695. self:display_inputbox("Search music on Jamendo",
  696. what.display, callback)
  697. end
  698. end
  699. -- Checks if the current playlist has changed after the last check.
  700. function awesompd:check_list()
  701. local bus = io.popen(self:mpcquery(true) .. "playlist")
  702. local info = bus:read("*all")
  703. bus:close()
  704. if info ~= self.list_line then
  705. self.list_line = info
  706. if string.len(info) > 0 then
  707. self.list_array = self.split(string.sub(info,1,string.len(info)))
  708. else
  709. self.list_array = {}
  710. end
  711. self.recreate_menu = true
  712. self.recreate_list = true
  713. end
  714. end
  715. -- Checks if the collection of playlists changed after the last check.
  716. function awesompd:check_playlists()
  717. local bus = io.popen(self:mpcquery(true) .. "lsplaylists")
  718. local info = bus:read("*all")
  719. bus:close()
  720. if info ~= self.playlists_line then
  721. self.playlists_line = info
  722. if string.len(info) > 0 then
  723. self.playlists_array = self.split(info)
  724. else
  725. self.playlists_array = {}
  726. end
  727. self.recreate_menu = true
  728. self.recreate_playlists = true
  729. end
  730. end
  731. -- Changes the current server to the specified one.
  732. function awesompd:change_server(server_number)
  733. self.current_server = server_number
  734. self:hide_notification()
  735. self.recreate_menu = true
  736. self.recreate_playback = true
  737. self.recreate_list = true
  738. self.recreate_playlists = true
  739. self.recreate_servers = true
  740. self:update_track()
  741. end
  742. function awesompd:add_jamendo_tracks(track_table)
  743. for i = 1, #track_table do
  744. self:command("add '" .. string.gsub(track_table[i].stream, '\\/', '/') .. "'")
  745. end
  746. self.recreate_menu = true
  747. self.recreate_list = true
  748. end
  749. -- /// End of menu generation functions ///
  750. function awesompd:show_notification(hint_title, hint_text, hint_image)
  751. self:hide_notification()
  752. self.notification = naughty.notify({ title = hint_title
  753. , text = awesompd.protect_string(hint_text)
  754. , timeout = 5
  755. , position = "top_right"
  756. , icon = hint_image
  757. , icon_size = self.album_cover_size
  758. })
  759. end
  760. function awesompd:hide_notification()
  761. if self.notification ~= nil then
  762. naughty.destroy(self.notification)
  763. self.notification = nil
  764. end
  765. end
  766. function awesompd:notify_track()
  767. if self:playing_or_paused() then
  768. local caption = self.status_text
  769. local nf_text = self.get_display_name(self.current_track)
  770. local al_cover = nil
  771. if self.show_album_cover then
  772. nf_text = self.get_extended_info(self.current_track)
  773. al_cover = self.current_track.album_cover
  774. end
  775. self:show_notification(caption, nf_text, al_cover)
  776. end
  777. end
  778. function awesompd:notify_state(state_changed)
  779. state_array = { "Volume: " .. self.state_volume ,
  780. "Repeat: " .. self.state_repeat ,
  781. "Random: " .. self.state_random ,
  782. "Single: " .. self.state_single ,
  783. "Consume: " .. self.state_consume }
  784. state_header = state_array[state_changed]
  785. table.remove(state_array,state_changed)
  786. full_state = state_array[1]
  787. for i = 2, #state_array do
  788. full_state = full_state .. "\n" .. state_array[i]
  789. end
  790. self:show_notification(state_header, full_state)
  791. end
  792. function awesompd:wrap_output(text)
  793. return format('<span font="%s" color="%s" background="%s">%s%s%s</span>',
  794. self.font, self.font_color, self.background,
  795. (text == "" and "" or self.ldecorator), awesompd.protect_string(text),
  796. (text == "" and "" or self.rdecorator))
  797. end
  798. -- This function actually sets the text on the widget.
  799. function awesompd:set_text(text)
  800. self.text_widget:set_markup(self:wrap_output(text))
  801. end
  802. function awesompd.find_pattern(text, pattern, start)
  803. return utf8.sub(text, string.find(text, pattern, start))
  804. end
  805. -- Scroll the given text by the current number of symbols.
  806. function awesompd:scroll_text(text)
  807. local result = text
  808. if self.scrolling then
  809. if self.output_size < utf8.len(text) then
  810. text = text .. " - "
  811. if self.scroll_pos + self.output_size - 1 > utf8.len(text) then
  812. result = utf8.sub(text, self.scroll_pos)
  813. result = result .. utf8.sub(text, 1, self.scroll_pos + self.output_size - 1 - utf8.len(text))
  814. self.scroll_pos = self.scroll_pos + 1
  815. if self.scroll_pos > utf8.len(text) then
  816. self.scroll_pos = 1
  817. end
  818. else
  819. result = utf8.sub(text, self.scroll_pos, self.scroll_pos + self.output_size - 1)
  820. self.scroll_pos = self.scroll_pos + 1
  821. end
  822. end
  823. end
  824. return result
  825. end
  826. -- This function is called every second.
  827. function awesompd:update_widget()
  828. self:set_text(self:scroll_text(self.text))
  829. self:check_notify()
  830. end
  831. -- This function is called by update_track each time content of
  832. -- the widget must be changed.
  833. function awesompd:update_widget_text()
  834. if self:playing_or_paused() then
  835. self.text = self.get_display_name(self.current_track)
  836. else
  837. self.text = self.status
  838. end
  839. end
  840. -- Checks if notification should be shown and shows if positive.
  841. function awesompd:check_notify()
  842. if self.to_notify then
  843. self:notify_track()
  844. self.to_notify = false
  845. end
  846. end
  847. function awesompd:notify_connect()
  848. self:show_notification("Connected", "Connection established to " .. self.servers[self.current_server].server ..
  849. " on port " .. self.servers[self.current_server].port)
  850. end
  851. function awesompd:notify_disconnect()
  852. self:show_notification("Disconnected", "Cannot connect to " .. self.servers[self.current_server].server ..
  853. " on port " .. self.servers[self.current_server].port)
  854. end
  855. function awesompd:update_track(file)
  856. local file_exists = (file ~= nil)
  857. if not file_exists then
  858. file = io.popen(self:mpcquery())
  859. end
  860. local track_line = file:read("*line")
  861. local status_line = file:read("*line")
  862. local options_line = file:read("*line")
  863. if not file_exists then
  864. file:close()
  865. end
  866. if not track_line or string.len(track_line) == 0 then
  867. if self.status ~= awesompd.DISCONNECTED then
  868. self:notify_disconnect()
  869. self.recreate_menu = true
  870. self.status = awesompd.DISCONNECTED
  871. self.current_track = { }
  872. self:update_widget_text()
  873. end
  874. else
  875. if self.status == awesompd.DISCONNECTED then
  876. self:notify_connect()
  877. self.recreate_menu = true
  878. self:update_widget_text()
  879. end
  880. if string.find(track_line,"volume:") or string.find(track_line,"Updating DB") then
  881. if self.status ~= awesompd.STOPPED then
  882. self.status = awesompd.STOPPED
  883. self.current_number = 0
  884. self.recreate_menu = true
  885. self.recreate_playback = true
  886. self.recreate_list = true
  887. self.album_cover = nil
  888. self.current_track = { }
  889. self:update_widget_text()
  890. end
  891. self:update_state(track_line)
  892. else
  893. self:update_state(options_line)
  894. local _, _, new_file, station, title, artist, album =
  895. string.find(track_line, "(.*)%-<>%-(.*)%-<>%-(.*)%-<>%-(.*)%-<>%-(.*)")
  896. local display_name, force_update = artist .. " - " .. title, false
  897. -- The following code checks if the current track is an
  898. -- Internet link. Internet radios change tracks, but the
  899. -- current file stays the same, so we should manually compare
  900. -- its title.
  901. if string.match(new_file, "http://") and
  902. -- The following line is awful. This needs to be replaced ASAP.
  903. not string.match(new_file, "http://storage%-new%.newjamendo%.com") then
  904. album = non_empty(station) or ""
  905. display_name = non_empty(title) or new_file
  906. if display_name ~= self.current_track.display_name then
  907. force_update = true
  908. end
  909. end
  910. if new_file ~= self.current_track.unique_name or force_update then
  911. self.current_track = jamendo.get_track_by_link(new_file)
  912. if not self.current_track then
  913. self.current_track = { display_name = display_name,
  914. album_name = album }
  915. end
  916. self.current_track.unique_name = new_file
  917. if self.show_album_cover then
  918. self.current_track.album_cover = self:get_cover(new_file)
  919. end
  920. self.to_notify = true
  921. self.recreate_menu = true
  922. self.recreate_playback = true
  923. self.recreate_list = true
  924. self.current_number = tonumber(self.find_pattern(status_line,"%d+"))
  925. self:update_widget_text()
  926. -- If the track is not the last, asynchronously download
  927. -- the cover for the next track.
  928. if self.list_array and self.current_number ~= #self.list_array then
  929. -- Get the link (in case it is Jamendo stream) to the next track
  930. local next_track =
  931. self:command_read('playlist -f "%file%" | head -' ..
  932. self.current_number + 1 .. ' | tail -1', "*line")
  933. jamendo.try_get_cover_async(next_track)
  934. end
  935. end
  936. local tmp_pst = string.find(status_line,"%d+%:%d+%/")
  937. local progress = self.find_pattern(status_line,"%#%d+/%d+") .. " " .. string.sub(status_line,tmp_pst)
  938. local new_status = awesompd.PLAYING
  939. if string.find(status_line,"paused") then
  940. new_status = awesompd.PAUSED
  941. end
  942. if new_status ~= self.status then
  943. self.to_notify = true
  944. self.recreate_list = true
  945. self.status = new_status
  946. self:update_widget_text()
  947. end
  948. self.status_text = self.status .. " " .. progress
  949. end
  950. end
  951. end
  952. function awesompd:update_state(state_string)
  953. self.state_volume = self.find_pattern(state_string,"%d+%% ")
  954. if string.find(state_string,"repeat: on") then
  955. self.state_repeat = self:check_set_state(self.state_repeat, "on")
  956. else
  957. self.state_repeat = self:check_set_state(self.state_repeat, "off")
  958. end
  959. if string.find(state_string,"random: on") then
  960. self.state_random = self:check_set_state(self.state_random, "on")
  961. else
  962. self.state_random = self:check_set_state(self.state_random, "off")
  963. end
  964. if string.find(state_string,"single: on") then
  965. self.state_single = self:check_set_state(self.state_single, "on")
  966. else
  967. self.state_single = self:check_set_state(self.state_single, "off")
  968. end
  969. if string.find(state_string,"consume: on") then
  970. self.state_consume = self:check_set_state(self.state_consume, "on")
  971. else
  972. self.state_consume = self:check_set_state(self.state_consume, "off")
  973. end
  974. end
  975. function awesompd:check_set_state(statevar, val)
  976. if statevar ~= val then
  977. self.recreate_menu = true
  978. self.recreate_options = true
  979. end
  980. return val
  981. end
  982. function awesompd:run_prompt(welcome,hook)
  983. awful.prompt.run({ prompt = welcome },
  984. self.promptbox[mouse.screen].widget,
  985. hook)
  986. end
  987. -- Replaces control characters with escaped ones.
  988. -- for_menu - defines if the special escable table for menus should be
  989. -- used.
  990. function awesompd.protect_string(str, for_menu)
  991. if for_menu then
  992. return utf8.replace(str, awesompd.ESCAPE_MENU_SYMBOL_MAPPING)
  993. else
  994. return utf8.replace(str, awesompd.ESCAPE_SYMBOL_MAPPING)
  995. end
  996. end
  997. -- Initialize the inputbox.
  998. function awesompd:init_inputbox()
  999. local width = 200
  1000. local height = 30
  1001. local border_color = beautiful.bg_focus or '#535d6c'
  1002. local margin = 4
  1003. local wbox = wibox({ name = "awmpd_ibox", height = height , width = width,
  1004. border_color = border_color, border_width = 1 })
  1005. local ws = screen[mouse.screen].workarea
  1006. wbox.screen = mouse.screen
  1007. wbox.ontop = true
  1008. local wprompt = awful.widget.prompt()
  1009. local wtbox = wibox.widget.textbox()
  1010. local wtmarginbox = wibox.layout.margin(wtbox, margin)
  1011. local tw, th = wtbox:fit(-1, -1)
  1012. wbox:geometry({ x = ws.width - width - 5, y = 25,
  1013. width = 200, height = th * 2 + margin})
  1014. local layout = wibox.layout.flex.vertical()
  1015. layout:add(wtmarginbox)
  1016. layout:add(wprompt)
  1017. wbox:set_widget(layout)
  1018. self.inputbox = { wibox = wbox,
  1019. title = wtbox,
  1020. prompt = wprompt }
  1021. end
  1022. -- Displays an inputbox on the screen (looks like naughty with prompt).
  1023. -- title_text - bold text on the first line
  1024. -- prompt_text - preceding text on the second line
  1025. -- hook - function that will be called with input data
  1026. -- Use it like this:
  1027. -- self:display_inputbox("Search music on Jamendo", "Artist", print)
  1028. function awesompd:display_inputbox(title_text, prompt_text, hook)
  1029. if not self.inputbox then
  1030. self:init_inputbox()
  1031. end
  1032. if self.inputbox.wibox.visible then -- Inputbox already exists, replace it
  1033. keygrabber.stop()
  1034. end
  1035. local exe_callback = function(s)
  1036. hook(s)
  1037. self.inputbox.wibox.visible = false
  1038. end
  1039. local done_callback = function()
  1040. self.inputbox.wibox.visible = false
  1041. end
  1042. self.inputbox.title:set_markup("<b>" .. title_text .. "</b>")
  1043. awful.prompt.run( { prompt = " " .. prompt_text .. ": ", bg_cursor = "#222222" },
  1044. self.inputbox.prompt.widget,
  1045. exe_callback, nil, nil, nil, done_callback, nil, nil)
  1046. self.inputbox.wibox.visible = true
  1047. end
  1048. -- Gets the cover for the given track. First looks in the Jamendo
  1049. -- cache. If the track is not a Jamendo stream, looks in local
  1050. -- folders. If there is no cover art either returns the default album
  1051. -- cover.
  1052. function awesompd:get_cover(track)
  1053. local radio_cover = nil
  1054. if self.radio_covers then
  1055. for station, cover in pairs(self.radio_covers) do
  1056. if track:match(station) then
  1057. radio_cover = cover
  1058. break
  1059. end
  1060. end
  1061. end
  1062. return radio_cover or jamendo.try_get_cover(track) or
  1063. self:try_get_local_cover(track) or self.ICONS.DEFAULT_ALBUM_COVER
  1064. end
  1065. -- Tries to find an album cover for the track that is currently
  1066. -- playing.
  1067. function awesompd:try_get_local_cover(current_file)
  1068. if self.mpd_config then
  1069. local result
  1070. -- First find the music directory in MPD configuration file
  1071. local _, _, music_folder = string.find(
  1072. self.pread('cat ' .. self.mpd_config .. ' | grep -v "#" | grep music_directory', "*line"),
  1073. 'music_directory%s+"(.+)"')
  1074. music_folder = music_folder .. "/"
  1075. -- If the music_folder is specified with ~ at the beginning,
  1076. -- replace it with user home directory
  1077. if string.sub(music_folder, 1, 1) == "~" then
  1078. local user_folder = self.pread("echo ~", "*line")
  1079. music_folder = user_folder .. string.sub(music_folder, 2)
  1080. end
  1081. -- Get the path to the file currently playing.
  1082. local _, _, current_file_folder = string.find(current_file, '(.+%/).*')
  1083. -- Check if the current file is not some kind of http stream or
  1084. -- Spotify track (like spotify:track:5r65GeuIoebfJB5sLcuPoC)
  1085. if not current_file_folder or string.match(current_file, "%w+://") then
  1086. return -- Let the default image to be the cover
  1087. end
  1088. local folder = music_folder .. current_file_folder
  1089. -- Get all images in the folder. Also escape occasional single
  1090. -- quotes in folder name.
  1091. local request = format("ls '%s' | grep -P '\\.jpg|\\.png|\\.gif|\\.jpeg'",
  1092. string.gsub(folder, "'", "'\\''"))
  1093. local covers = self.pread(request, "*all")
  1094. local covers_table = self.split(covers)
  1095. if covers_table.n > 0 then
  1096. result = folder .. covers_table[1]
  1097. if covers_table.n > 1 then
  1098. -- Searching for front cover with grep because Lua regular
  1099. -- expressions suck:[
  1100. local front_cover =
  1101. self.pread('echo "' .. covers ..
  1102. '" | grep -P -i "cover|front|folder|albumart" | head -n 1', "*line")
  1103. if front_cover then
  1104. result = folder .. front_cover
  1105. end
  1106. end
  1107. end
  1108. return result
  1109. end
  1110. end
  1111. -- /// Deprecated, left for some backward compatibility in
  1112. -- configuration ///
  1113. function awesompd:command_toggle()
  1114. return self:command_playpause()
  1115. end
  1116. return awesompd