xmms2-openboxmenu.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. #!/usr/bin/env python2
  2. #-*- coding:utf-8 -*-
  3. # Copyright (c) 2012 Eli
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  6. # associated documentation files (the "Software"), to deal in the Software without restriction,
  7. # including without limitation the rights to use, copy, modify, merge, publish, distribute,
  8. # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  9. # furnished to do so, subject to the following conditions:
  10. #
  11. # The above copyright notice and this permission notice shall be included
  12. # in all copies or substantial portions of the Software.
  13. #
  14. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
  15. # BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  16. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  17. # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  19. #===============================================================================
  20. #Openbox menu writers
  21. def marker(isMarked):
  22. if isMarked is None:
  23. return ""
  24. if isMarked:
  25. return "=> "
  26. else:
  27. return ". "
  28. class Label():
  29. def __init__(self, label, isMarked=None):
  30. self.label = label
  31. self.isMarked = isMarked
  32. def write(self):
  33. formattedLabel = quoteattr(marker(self.isMarked) + self.label)
  34. print("<item label={0}>".format(formattedLabel))
  35. print("</item>")
  36. class Button():
  37. def __init__(self, label, commands, isMarked=None):
  38. self.label = label
  39. self.commands = commands
  40. self.isMarked = isMarked
  41. def write(self):
  42. formattedLabel = marker(self.isMarked) + self.label
  43. formattedLabel = quoteattr(formattedLabel)
  44. command = createCommand(self.commands)
  45. print("<item label={0}>".format(formattedLabel))
  46. print(" <action name=\"Execute\">")
  47. print(" <execute>{0}</execute>".format(command))
  48. print(" </action>")
  49. print("</item>")
  50. class Menu():
  51. def __init__(self, id, label, entries=None, isMarked=None):
  52. self.id = id
  53. self.label = label
  54. self.entries = entries
  55. self.isMarked = isMarked
  56. def write(self):
  57. formattedMarker = marker(self.isMarked) + self.label
  58. print("<menu id={0} label={1}>".format(quoteattr(self.id),
  59. quoteattr(formattedMarker)))
  60. for entry in self.entries:
  61. if entry is not None:
  62. entry.write()
  63. print("</menu>")
  64. class PipeMenu():
  65. def __init__(self, label, commands, isMarked=None):
  66. self.label = label
  67. self.commands = commands
  68. self.isMarked = isMarked
  69. def write(self):
  70. formattedLabel = quoteattr(marker(self.isMarked) + self.label)
  71. command = createCommand(self.commands)
  72. print("<menu execute={0} id={1} label={2}/>".format(quoteattr(command),
  73. quoteattr(command),
  74. formattedLabel))
  75. class Separator():
  76. def __init__(self, label=None):
  77. self.label = label
  78. def write(self):
  79. if self.label is None:
  80. print("<separator/>")
  81. else:
  82. print("<separator label={0}/>".format(quoteattr(self.label)))
  83. class Container():
  84. def __init__(self, entries):
  85. self.entries = entries
  86. def write(self):
  87. print("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
  88. print("<openbox_pipe_menu>")
  89. if isinstance(self.entries, list):
  90. for entry in self.entries:
  91. if entry is not None:
  92. entry.write()
  93. else:
  94. self.entries.write()
  95. print("</openbox_pipe_menu>")
  96. #===============================================================================
  97. #Imports
  98. import os
  99. import sys
  100. from pipes import quote
  101. from urllib import unquote_plus
  102. from xml.sax.saxutils import escape, unescape, quoteattr
  103. import ConfigParser
  104. try:
  105. import Tkinter
  106. import tkSimpleDialog
  107. import xmmsclient
  108. from xmmsclient import collections as xc
  109. except ImportError as error:
  110. Container([Separator("Failed to load required modules!"), Separator(str(error)) ]).write()
  111. sys.exit(1)
  112. #===============================================================================
  113. #Helper Methods
  114. def createCommand(parameters):
  115. return __file__ + ' ' + ' '.join([quoteattr(str(i)) for i in parameters])
  116. def humanReadableSize(size):
  117. for x in ['bytes','KB','MB','GB']:
  118. if size < 1024.0:
  119. return "%3.2f%s" % (size, x)
  120. size /= 1024.0
  121. def humanReadableDuration(milliseconds):
  122. seconds = int(milliseconds) / 1000
  123. minutes, seconds = divmod(seconds, 60)
  124. if minutes > 0:
  125. return "{0}m {1}s".format(minutes, seconds)
  126. else:
  127. return "{1}s".format(seconds)
  128. def readString(dictionary, key, default=""):
  129. if key in dictionary:
  130. value = dictionary[key]
  131. if isinstance(value, basestring):
  132. return value.encode('utf8')
  133. else:
  134. return str(value)
  135. else:
  136. return default
  137. #===============================================================================
  138. #Writers
  139. class AlphabetIndex():
  140. def write(self):
  141. indexKeys = map(chr, range(65, 91))
  142. for key in indexKeys:
  143. artist = xc.Match( field="artist", value= str(key)+"*" )
  144. results = xmms.coll_query_infos( artist, ["artist"])
  145. groupLabel = "{0} ({1})".format(str(key), str(len(results)))
  146. PipeMenu(groupLabel, ["alphabetIndexArtists", str(key)] ).write()
  147. class ArtistsList():
  148. def __init__(self, artist):
  149. self.artistMatch = xc.Match( field="artist", value= str(artist)+"*" )
  150. def write(self):
  151. results = xmms.coll_query_infos(self.artistMatch, ["artist"] )
  152. for result in results:
  153. artist = readString(result, 'artist')
  154. PipeMenu(artist, ["indexAlbum", artist] ).write()
  155. class AlbumList():
  156. def __init__(self, artist):
  157. self.artist = artist
  158. self.artistMatch = xc.Match(field="artist", value=artist)
  159. def write(self):
  160. results = xmms.coll_query_infos(self.artistMatch, ["date", "album"] )
  161. for result in results:
  162. if result["album"] is not None:
  163. album = readString(result, 'album')
  164. label = "[" + readString(result, 'date') + "] " + album
  165. PipeMenu(label, ["indexTracks", self.artist, album] ).write()
  166. class TrackList():
  167. def __init__(self, artist, album):
  168. self.artist = artist
  169. self.album = album
  170. self.match = xc.Intersection(xc.Match(field="artist", value=self.artist),
  171. xc.Match(field="album", value=self.album))
  172. def write(self):
  173. results = xmms.coll_query_infos( self.match, ["tracknr", "title", "id"])
  174. counter = 0
  175. for result in results:
  176. id = str(result["id"])
  177. title = readString(result, 'title')
  178. trackNumber = readString(result, 'tracknr')
  179. addToCurrentPlaylist = Button("Add to Playlist", ["track", "add", str(id)] )
  180. trackInfo = PipeMenu("Infos", ["track", "info", str(id)] )
  181. Menu("xmms-track-"+id, trackNumber + " - " + title, [addToCurrentPlaylist, trackInfo]).write()
  182. counter +=1
  183. Separator().write()
  184. Button("Add to Playlist", ["album", "add", self.artist, self.album] ).write()
  185. class TrackInfo():
  186. def __init__(self, id):
  187. self.id = int(id)
  188. def write(self):
  189. minfo = xmms.medialib_get_info(self.id)
  190. Label("Artist \t: " + readString(minfo, 'artist')).write()
  191. Label("Album \t: " + readString(minfo, 'album')).write()
  192. Label("Title \t: " + readString(minfo, 'title')).write()
  193. Label("Duration \t: " + humanReadableDuration(minfo['duration'])).write()
  194. Separator().write()
  195. Label("Size \t\t: " + humanReadableSize(minfo["size"])).write()
  196. Label("Bitrate \t: " + readString(minfo, 'bitrate')).write()
  197. url = unquote_plus(readString(minfo, 'url'))
  198. filename = url.split('/')[-1]
  199. Label("Url \t: " + url).write()
  200. Label("File \t: " + filename).write()
  201. class ConfigMenu():
  202. def write(self):
  203. Separator("Presets:").write()
  204. ConfigPresets().write()
  205. Separator().write()
  206. ConfigView().write()
  207. class ConfigPresets():
  208. def __init__(self):
  209. xmmsDirectory = xmmsclient.userconfdir_get()
  210. configPath = os.path.join(xmmsDirectory, "clients/openboxMenu/configPresets.ini")
  211. self.errorMessage = None
  212. self.config = ConfigParser.RawConfigParser()
  213. try:
  214. result = self.config.read(configPath)
  215. if len(result) != 1:
  216. self.errorMessage = 'Preset file not found'
  217. except ConfigParser.ParsingError as error:
  218. self.errorMessage = 'Preset file parsing error'
  219. def load(self, name):
  220. for key, value in self.config.items(name):
  221. xmms.config_set_value(key, value)
  222. def write(self):
  223. if self.errorMessage != None:
  224. Separator(self.errorMessage).write()
  225. return
  226. for preset in self.config.sections():
  227. isActive = True
  228. for key, value in self.config.items(preset):
  229. actualValue = xmms.config_get_value(key)
  230. if value != actualValue:
  231. isActive = False
  232. break
  233. Button(preset, ["preset-load", preset], isActive).write()
  234. class ConfigView():
  235. def __init__(self, configKey = None):
  236. self.configKey = configKey
  237. def write(self):
  238. resultData = xmms.config_list_values();
  239. if self.configKey is None:
  240. namespaces = set()
  241. submenues = list()
  242. for entry in resultData:
  243. namespaces.add(entry.split('.')[0])
  244. for setEntry in namespaces:
  245. submenues.append(PipeMenu(setEntry, ["menu", "config-view", str(setEntry)] ))
  246. Menu("view all", "configView", submenues).write()
  247. else:
  248. namespaces = list()
  249. for entry in resultData:
  250. if entry.startswith(self.configKey):
  251. namespaces.append(entry)
  252. namespaces.sort()
  253. displayKeyChars = 0
  254. for entry in namespaces:
  255. displayKeyChars = max(displayKeyChars, len(entry))
  256. print(len(entry))
  257. print(displayKeyChars)
  258. for entry in namespaces:
  259. padding = displayKeyChars - len(entry) + 1
  260. Label(entry + (" " * padding) + "\t" + resultData[entry]).write()
  261. class VolumeMenu():
  262. def write(self):
  263. if xmms.playback_status() == xmmsclient.PLAYBACK_STATUS_STOP:
  264. Separator("Cannot set Volume on Stopped stream.").write()
  265. return
  266. currentVolumes = xmms.playback_volume_get()
  267. masterVolume = currentVolumes['master']
  268. volumes = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
  269. volumeHasBeenSelected = False
  270. for id, val in enumerate(volumes):
  271. isSelectedVolume = False
  272. if masterVolume <= val and not volumeHasBeenSelected:
  273. isSelectedVolume = True
  274. volumeHasBeenSelected = True
  275. Button(str(val)+"%", ["volume", val], isSelectedVolume).write()
  276. class PlaylistMenu():
  277. def write(self):
  278. playlists = xmms.playlist_list()
  279. activePlaylist = xmms.playlist_current_active()
  280. playlistMenu = list()
  281. playlistMenu.append(Button("New Playlist", ["createPlaylist"] ))
  282. playlistMenu.append(Separator())
  283. for playlist in playlists:
  284. if playlist.startswith('_'):
  285. continue
  286. loadButton = Button("load", ["loadPlaylist", playlist] )
  287. deleteButton = Button("delete", ["removePlaylist", playlist] )
  288. playlistMenu.append(Menu("xmms-playlist-"+playlist, playlist, [loadButton, Separator(), deleteButton], playlist == activePlaylist))
  289. Menu("xmms-playlists", "Playlist: {0}".format(activePlaylist), playlistMenu).write()
  290. Separator().write()
  291. displayRange = 10
  292. activeId = xmms.playback_current_id()
  293. activePlaylistIds = xmms.playlist_list_entries()
  294. if (activePlaylistIds != None) and (activePlaylistIds.count(activeId) == 1):
  295. selectedIndex = activePlaylistIds.index(activeId)
  296. PlaylistEntriesMenu(selectedIndex, "both", displayRange).write()
  297. else:
  298. PlaylistEntriesMenu(0, "top", displayRange).write()
  299. class PlaylistEntriesMenu():
  300. def __init__(self, pos, expandDirection, maxDisplayed = 50):
  301. self.entryIds = xmms.playlist_list_entries()
  302. if self.entryIds is None:
  303. return
  304. self.expandBottom = False
  305. self.expandTop = False
  306. if expandDirection == "bottom":
  307. if pos - maxDisplayed > 0:
  308. self.expandBottom = True
  309. self.positions = range(max(pos - maxDisplayed, 0), pos)
  310. if expandDirection == "top":
  311. if pos + maxDisplayed < len(self.entryIds):
  312. self.expandTop = True
  313. self.positions = range(pos, min(pos + maxDisplayed, len(self.entryIds)))
  314. if expandDirection == "both":
  315. halfDisplayed = maxDisplayed/2
  316. if pos - halfDisplayed > 0:
  317. self.expandBottom = True
  318. if pos + halfDisplayed < len(self.entryIds):
  319. self.expandTop = True
  320. self.positions = range(max(pos - halfDisplayed, 0), min(pos + halfDisplayed, len(self.entryIds)))
  321. def write(self):
  322. if self.entryIds is None:
  323. Label('Playlist is Empty').write()
  324. return
  325. if self.expandBottom:
  326. PipeMenu("... before", ["menu", "playlist-entries", str(self.positions[0]), "bottom"] ).write()
  327. try:
  328. currentPosition = xmms.playlist_current_pos()
  329. except:
  330. currentPosition = None
  331. for id in self.positions:
  332. activeId = xmms.playback_current_id()
  333. medialibId = self.entryIds[id]
  334. result = xmms.medialib_get_info(medialibId)
  335. artist = readString(result, 'artist')
  336. album = readString(result, 'album')
  337. title = readString(result, 'title')
  338. subMenuId = "xmms-activePlaylist-" + str(medialibId)
  339. entryLabel = "{0}| {1} - {2} - {3}".format(
  340. str(id).zfill(3), artist, album, title)
  341. moveMenu = Menu("xmms-move-" + str(medialibId), "move",
  342. [
  343. Button("move first", ["playlist-entry", "move", str(id), str(0)] ),
  344. Button("move -5", ["playlist-entry", "move", str(id), str(id - 5)] ),
  345. Button("move -1", ["playlist-entry", "move", str(id), str(id - 1)] ),
  346. Button("move +1", ["playlist-entry", "move", str(id), str(id + 1)] ),
  347. Button("move +5", ["playlist-entry", "move", str(id), str(id + 5)] ),
  348. Button("move last", ["playlist-entry", "move", str(id), str(len(self.entryIds) - 1)] )
  349. ])
  350. subMenu = Menu(subMenuId, entryLabel,
  351. [
  352. Button("jump", ["jump", str(id)] ),
  353. Separator(),
  354. moveMenu,
  355. Separator(),
  356. PipeMenu("Infos", ["track", "info", str(medialibId)] ),
  357. Separator(),
  358. Button("delete", ["playlist-entry", "remove", str(id)] )
  359. ],
  360. medialibId == activeId ).write()
  361. if self.expandTop:
  362. PipeMenu("... after", ["menu", "playlist-entries", str(self.positions[-1]+1), "top"]).write()
  363. #===============================================================================
  364. #Main Menu
  365. class MainMenu():
  366. def write(self):
  367. if xmms.playback_status() == xmmsclient.PLAYBACK_STATUS_PLAY:
  368. Button("⧐ Pause", ["pause"] ).write()
  369. else:
  370. Button("⧐ Play", ["play"] ).write()
  371. Button("≫ next", ["next"] ).write()
  372. Button("≪ prev", ["prev"] ).write()
  373. Separator().write()
  374. PipeMenu("Volume", ["menu", "volume"] ).write()
  375. Separator().write()
  376. PipeMenu("Medialib", ["menu", "index-alphabet"] ).write()
  377. PipeMenu("Config", ["menu", "config"] ).write()
  378. Separator().write()
  379. PlaylistMenu().write()
  380. #===============================================================================
  381. #Commands
  382. def createPlaylist():
  383. root = Tkinter.Tk()
  384. root.withdraw()
  385. name = tkSimpleDialog.askstring("New Playlist Name",
  386. "Enter a new Playlist Name")
  387. if name is not None:
  388. xmms.playlist_create(name)
  389. #===============================================================================
  390. #Main
  391. if __name__ == "__main__":
  392. xmms = xmmsclient.XMMSSync("xmms2-OpenboxMenu")
  393. try:
  394. xmms.connect(os.getenv("XMMS_PATH"))
  395. except IOError as detail:
  396. Container(Separator("Connection failed: "+ str(detail))).write()
  397. sys.exit(1)
  398. paramterCount = len(sys.argv)
  399. if paramterCount == 1:
  400. Container(MainMenu()).write()
  401. elif paramterCount >= 2:
  402. command = sys.argv[1]
  403. if command == "menu":
  404. menuName = str(sys.argv[2])
  405. if menuName == "playlist-entries":
  406. pos = int(sys.argv[3])
  407. direction = str(sys.argv[4])
  408. Container(PlaylistEntriesMenu(pos, direction)).write()
  409. if menuName == "volume":
  410. Container(VolumeMenu()).write()
  411. if menuName == "config":
  412. Container(ConfigMenu()).write()
  413. if menuName == "config-view":
  414. configKey = None
  415. if paramterCount == 4:
  416. configKey = str(sys.argv[3])
  417. Container(ConfigView(configKey)).write()
  418. if menuName == "index-alphabet":
  419. Container(AlphabetIndex()).write()
  420. if command == "play":
  421. xmms.playback_start()
  422. if command == "pause":
  423. xmms.playback_pause()
  424. if command == "next":
  425. xmms.playlist_set_next_rel(1)
  426. xmms.playback_tickle()
  427. if command == "prev":
  428. xmms.playlist_set_next_rel(-1)
  429. xmms.playback_tickle()
  430. if command == "jump":
  431. position = int(sys.argv[2])
  432. xmms.playlist_set_next(position)
  433. xmms.playback_tickle()
  434. if command == "track":
  435. trackCommand = str(sys.argv[2])
  436. trackId = int(sys.argv[3])
  437. if trackCommand == "add":
  438. xmms.playlist_insert_id(0, trackId)
  439. if trackCommand == "info":
  440. Container(TrackInfo(trackId)).write()
  441. if command == "album":
  442. albumCommand = str(sys.argv[2])
  443. artistName = str(sys.argv[3])
  444. albumName = str(sys.argv[4])
  445. if albumCommand == "add":
  446. match = xc.Intersection(xc.Match(field="artist", value=artistName),
  447. xc.Match(field="album", value=albumName))
  448. trackIds = xmms.coll_query_infos( match, ["id"])
  449. for trackId in trackIds:
  450. xmms.playlist_add_id(trackId["id"])
  451. if command == "playlist-entry":
  452. subCommand = str(sys.argv[2])
  453. entryIndex = int(sys.argv[3])
  454. if subCommand == "move":
  455. newIndex = int(sys.argv[4])
  456. xmms.playlist_move(entryIndex, newIndex)
  457. if subCommand == "remove":
  458. xmms.playlist_remove_entry(entryIndex)
  459. if command == "createPlaylist":
  460. createPlaylist()
  461. if command == "loadPlaylist":
  462. playlistName = str(sys.argv[2])
  463. xmms.playlist_load(playlistName)
  464. if command == "removePlaylist":
  465. playlistName = str(sys.argv[2])
  466. xmms.playlist_remove(playlistName)
  467. if command == "preset-load":
  468. presetName = str(sys.argv[2])
  469. ConfigPresets().load(presetName)
  470. if command == "volume":
  471. volume = int(sys.argv[2])
  472. xmms.playback_volume_set("master", volume)
  473. if command == "alphabetIndexArtists":
  474. index = str(sys.argv[2])
  475. Container(ArtistsList(unescape(index))).write()
  476. if command == "indexAlbum":
  477. artist = str(sys.argv[2])
  478. Container(AlbumList(unescape(artist))).write()
  479. if command == "indexTracks":
  480. artist = str(sys.argv[2])
  481. album = str(sys.argv[3])
  482. Container(TrackList(unescape(artist), unescape(album))).write()