#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import line_profiler
import atexit
profile = line_profiler.LineProfiler()
atexit.register(profile.print_stats)
from sys import argv, path
import os
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
print(dname)
if "DEV_FILES" in dname:
print("Running in development mode")
os.chdir(dname)
else:
print("Running in production mode")
os.chdir("/usr/share/hbud/")
path.append("modules")
import gi, dbus, srt, azapi, dbus.mainloop.glib, json
from concurrent import futures
from time import sleep, time
from operator import itemgetter
from collections import deque
from datetime import timedelta
from random import sample
from configparser import ConfigParser
from mediafile import MediaFile
gi.require_version('Gtk', '3.0')
gi.require_version('Gst', '1.0')
from gi.repository import Gtk, Gst, GLib, GdkPixbuf, Gdk
class TrackBox(Gtk.EventBox):
@profile
def __init__(self, title, artist, id, year, length, album):
super(TrackBox, self).__init__()
self.set_can_focus(False)
self.set_name(f"trackbox_{id}")
self.set_size_request(-1, 60)
subBox = Gtk.Box.new(0, 5)
comboBox = Gtk.Box.new(1, 5)
tiLab = Gtk.Label.new()
alLab = Gtk.Label.new(album)
arLab = Gtk.Label.new(artist)
yeLab = Gtk.Label.new(str(year))
leLab = Gtk.Label.new()
leLab.set_markup(f'{length}')
tiLab.set_ellipsize(3)
tiLab.set_halign(Gtk.Align(1))
tiLab.set_margin_top(15)
tiLab.set_markup(f"{title}")
alLab.set_ellipsize(3)
alLab.set_halign(Gtk.Align(1))
alLab.set_margin_bottom(15)
arLab.set_ellipsize(3)
arLab.set_halign(Gtk.Align(1))
comboBox.pack_start(tiLab, False, False, 0)
comboBox.pack_start(alLab, False, False, 0)
subBox.pack_start(comboBox, True, True, 15)
subBox.pack_end(leLab, False, False, 10)
subBox.pack_end(yeLab, False, False, 20)
subBox.pack_end(arLab, False, False, 20)
self.add(subBox)
self.set_margin_end(15)
targs = [Gtk.TargetEntry.new("dummy", Gtk.TargetFlags.SAME_APP, 1)]
self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, targs, Gdk.DragAction.MOVE)
self.drag_dest_set(Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP | Gtk.DestDefaults.MOTION, targs, Gdk.DragAction.MOVE)
class GUI:
@profile
def __init__(self):
UI_FILE, version = "hbud.glade", "HBud 0.2.3 Yennefer"
self.useMode = "audio"
self.supportedList = ['.3gp', '.aa', '.aac', '.aax', '.aiff', '.flac', '.m4a', '.mp3', '.ogg', '.wav', '.wma', '.wv']
try:
self.clickedE = argv[1]
if os.path.splitext(self.clickedE)[-1] not in self.supportedList and os.path.splitext(self.clickedE)[-1] != "":
self.useMode = "video"
print("video, now", self.clickedE)
except:
self.clickedE = False
self.builder = Gtk.Builder()
self.builder.add_from_file(UI_FILE)
self.builder.connect_signals(self)
whview = self.builder.get_object("whView")
buffer = Gtk.TextBuffer()
buffer.set_text("""
v0.2.3 - Oct 03 2021 :
* Modernized and revamped the settings menu
* Added option to change accent color
* Fixed packaging issues
* Fixed some bugs
* Added some shortcuts (double click to fullscreen, etc.)
* Added release notes to about section""")
whview.set_buffer(buffer)
self.playlistPlayer, self.needSub, self.nowIn = False, False, ""
self.DAPI = azapi.AZlyrics('duckduckgo', accuracy=0.65)
self.fulle, self.resete, self.keepReset, self.hardReset, self.tnum, self.sorted = False, False, False, False, 0, False
self.sSize, self.sMarg = int(float(sSize)), int(float(sMarg))
self.size, self.size2, self.size3, self.size4 = 35000, 15000, self.sSize*450, float(f"0.0{self.sMarg}")*450
self.sub, self.seekBack, self.playing, self.res = self.builder.get_object('sub'), False, False, False
if dark == "False": self.darke = False
else: self.darke = True
if bg == "False": self.bge = False
else: self.bge = True
self.slider = Gtk.HScale()
self.slider.set_can_focus(False)
self.slider.set_margin_start(6)
self.slider.set_margin_end(6)
self.slider.set_draw_value(False)
self.slider.set_increments(1, 10)
self.slider_handler_id = self.slider.connect("value-changed", self.on_slider_seek)
self.box = self.builder.get_object("slidBox")
self.label = Gtk.Label(label='0:00')
self.label.set_margin_start(6)
self.label.set_margin_end(6)
self.label_end = Gtk.Label(label='0:00')
self.label_end.set_margin_start(6)
self.label_end.set_margin_end(6)
self.box.pack_start(self.label, False, False, 0)
self.box.pack_start(self.slider, True, True, 0)
self.box.pack_start(self.label_end, False, False, 0)
self.trackCover = self.builder.get_object("cover_img")
self.trackCover.set_name("cover_img")
self.plaicon = self.builder.get_object("play")
self.slider.connect("enter-notify-event", self.mouse_enter)
self.slider.connect("leave-notify-event", self.mouse_leave)
self.playlistBox = self.builder.get_object("expanded")
self.exBot = self.builder.get_object("extendedBottom")
self.header = self.builder.get_object("header")
self.label1 = self.builder.get_object('label1')
self.label2 = self.builder.get_object('label2')
self.label3 = self.builder.get_object('label3')
self.yrEnt = self.builder.get_object("yrEnt")
self.tiEnt = self.builder.get_object("tiEnt")
self.infBut = self.builder.get_object("infBut")
self.alEnt = self.builder.get_object("alEnt")
self.arEnt = self.builder.get_object("arEnt")
self.karaokeBut = self.builder.get_object("kar")
self.sub2 = self.builder.get_object("sub2")
self.shuffBut = self.builder.get_object("shuffBut")
self.locBut = self.builder.get_object("locBut")
self.strBut = self.builder.get_object("strBut")
self.mainStack = self.builder.get_object("mainStack")
self.strOverlay = self.builder.get_object("strOverlay")
self.theTitle = self.builder.get_object("theTitle")
self.subSpin = self.builder.get_object("subSpin")
self.subMarSpin = self.builder.get_object("subSpin1")
self.subcheck = self.builder.get_object("sub_check")
self.nosub = self.builder.get_object("nosub")
self.iChoser = self.builder.get_object("iChoser")
self.roundSpin = self.builder.get_object("round_spin")
self.colorer = self.builder.get_object("colorer")
self.color = color
coco = Gdk.RGBA()
coco.parse(self.color)
self.colorer.set_rgba(coco)
self.roundSpin.set_value(int(rounded))
self.dark_switch, self.bg_switch = self.builder.get_object("dark_switch"), self.builder.get_object("bg_switch")
self.dark_switch.set_state(self.darke)
self.bg_switch.set_state(self.bge)
self.topBox = self.builder.get_object("topBox")
self.drop_but = self.builder.get_object("drop_but")
image_filter = Gtk.FileFilter()
image_filter.set_name("Image files")
image_filter.add_mime_type("image/*")
self.iChoser.add_filter(image_filter)
GLib.idle_add(self.subcheck.hide)
GLib.idle_add(self.builder.get_object("oplink").set_label, "Visit OpenSubtitles")
GLib.idle_add(self.builder.get_object("sublink").set_label, "Visit Subscene")
self.subSpin.set_value(self.sSize)
self.subStack = self.builder.get_object("subStack")
self.lyrLab = self.builder.get_object("lyrLab")
self.karmode = self.builder.get_object("req_scroll")
self.lyrmode = self.builder.get_object("req_scroll2")
self.subMarSpin.set_value(self.sMarg)
self.window = self.builder.get_object('main')
self.sub.connect('size-allocate', self._on_size_allocated)
self.window.connect('size-allocate', self._on_size_allocated0)
self.switchDict = {"locBut" : [self.builder.get_object("placeholder"), "audio-input-microphone", "audio", self.strBut, self.infBut], "strBut" : [self.builder.get_object("strBox"), "view-fullscreen", "video", self.locBut, self.infBut], "infBut" : [self.builder.get_object("infBook"), "", "", self.locBut, self.strBut]}
self.provider, self.settings = Gtk.CssProvider(), Gtk.Settings.get_default()
self.themer(str(rounded))
self.settings.set_property("gtk-application-prefer-dark-theme", self.darke)
# Display the program
self.window.set_title(version)
self.window.show_all()
self.createPipeline("local")
self.topBox.hide()
self.drop_but.hide()
self.locBut.set_active(True)
if self.clickedE:
if self.useMode == "audio":
self.loader("xy")
self.on_playBut_clicked("xy")
else:
self.strBut.set_active(True)
self.on_playBut_clicked("xy")
self.listener() # Do not write anything after this in init
@profile
def highlight(self, widget, event):
if event.button == 3:
self.ednum = int(widget.get_name().replace("trackbox_", ""))
menu = Gtk.Menu()
menu.set_can_focus(False)
menu_item = Gtk.MenuItem.new_with_label('Delete from current playqueue')
menu_item.set_can_focus(False)
menu_item.connect("activate", self.del_cur)
menu.add(menu_item)
menu_item = Gtk.MenuItem.new_with_label('Edit metadata')
menu_item.set_can_focus(False)
menu_item.connect("activate", self.ed_cur)
menu.add(menu_item)
menu.show_all()
menu.popup_at_pointer()
else:
self.tnum = int(widget.get_name().replace("trackbox_", ""))
self.themer(self.roundSpin.get_value(), self.tnum)
self.on_next("clickMode")
@profile
def on_search(self, widget):
term = widget.get_text().lower()
if term != "":
results = []
for i, item in enumerate(self.playlist):
if term in item["title"].lower() or term in item["artist"].lower() or term in item["album"].lower():
results.append(i)
for i, item in enumerate(self.supBox.get_children()):
if i not in results: GLib.idle_add(item.hide)
else:
GLib.idle_add(self.supBox.show_all)
@profile
def on_sort_change(self, widget):
aid = int(widget.get_active_id())
if self.sorted == False: self.archive = self.playlist
self.sorted = True
if aid == 0 and self.sorted == True:
self.playlist = self.archive
self.sorted = False
elif aid == 1: self.playlist = sorted(self.archive, key=itemgetter('artist'),reverse=False)
elif aid == 2: self.playlist = sorted(self.archive, key=itemgetter('artist'),reverse=True)
elif aid == 3: self.playlist = sorted(self.archive, key=itemgetter('title'),reverse=False)
elif aid == 4: self.playlist = sorted(self.archive, key=itemgetter('title'),reverse=True)
elif aid == 5: self.playlist = sorted(self.archive, key=itemgetter('year'),reverse=False)
elif aid == 6: self.playlist = sorted(self.archive, key=itemgetter('year'),reverse=True)
elif aid == 7: self.playlist = sorted(self.archive, key=itemgetter('length'),reverse=False)
elif aid == 8: self.playlist = sorted(self.archive, key=itemgetter('length'),reverse=True)
self.neo_playlist_gen()
@profile
def on_clear_order(self, _): os.system(f"rm {self.folderPath}/.saved.order")
@profile
def on_rescan_order(self, _):
GLib.idle_add(self.loader, self.folderPath, True)
@profile
def on_order_save(self, _):
f = open(f"{self.folderPath}/.saved.order", "w+")
f.write(json.dumps(self.playlist))
f.close()
@profile
def themer(self, v, w=""):
css = """#cover_img {
padding-bottom: %spx;
}
menu, .popup {
border-radius: %spx;
}
decoration, headerbar {
border-radius: %spx;
}
button, menuitem, entry {
border-radius: %spx;
margin: 5px;
}
.titlebar, tab {
border-top-left-radius: %spx;
border-top-right-radius: %spx;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
window, notebook, stack, box, scrolledwindow, viewport {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-left-radius: %spx;
border-bottom-right-radius: %spx;
border-width: 0px;
border-image: none;
box-shadow: none;
}
switch:checked {
background-color: %s;
}
#trackbox_%s{
background: %s;
color: #000;
border-radius: %spx;
}
.maximized, .fullscreen, .maximized .titlebar {
border-radius: 0px;
}""" % (int(v)/2.6,int(v)/1.5,v,v,v,v,v,v,self.color,w,self.color,v) # decoration, window, window.background, window.titlebar, .titlebar
css = str.encode(css)
self.provider.load_from_data(css)
self.window.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self.provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
@profile
def createPipeline(self, mode):
if mode == "local":
self.videoPipe, self.audioPipe = Gst.ElementFactory.make("playbin3"), Gst.ElementFactory.make("playbin3")
# self.audioPipe = Gst.parse_launch('filesrc ! decodebin ! audioconvert ! rgvolume ! audioconvert ! audioresample ! alsasink')
playerFactory = self.videoPipe.get_factory()
gtksink = playerFactory.make('gtksink')
self.videoPipe.set_property("video-sink", gtksink)
gtksink.props.widget.set_valign(Gtk.Align.FILL)
gtksink.props.widget.set_halign(Gtk.Align.FILL)
gtksink.props.widget.connect("button_press_event", self.mouse_click0)
self.strOverlay.add(gtksink.props.widget)
gtksink.props.widget.show()
self.strOverlay.add_overlay(self.theTitle)
bus = self.videoPipe.get_bus()
bus.add_signal_watch()
bus.connect("message", self.on_message)
bus = self.audioPipe.get_bus()
bus.add_signal_watch()
bus.connect("message", self.on_message)
# elif mode == "stream":
# self.player = Gst.parse_launch(f"souphttpsrc is-live=false location={self.url} ! decodebin ! audioconvert ! autoaudiosink")
@profile
def on_dropped(self, _):
if self.topBox.get_visible() == True:
GLib.idle_add(self.drop_but.get_image().set_from_icon_name, "gtk-go-down", Gtk.IconSize.BUTTON)
GLib.idle_add(self.topBox.hide)
else:
GLib.idle_add(self.drop_but.get_image().set_from_icon_name, "gtk-go-up", Gtk.IconSize.BUTTON)
GLib.idle_add(self.topBox.show)
@profile
def allToggle(self, button):
btn = Gtk.Buildable.get_name(button)
if self.mainStack.get_visible_child() != self.switchDict[btn][0] and button.get_active() == True:
self.mainStack.set_visible_child(self.switchDict[btn][0])
if self.playing == True: self.on_playBut_clicked("xy")
if btn != "infBut":
GLib.idle_add(self.exBot.show)
if btn == "locBut":
GLib.idle_add(self.trackCover.show)
GLib.idle_add(self.shuffBut.show)
if self.playlistPlayer == True: GLib.idle_add(self.drop_but.show)
GLib.idle_add(self.subcheck.hide)
else:
GLib.idle_add(self.trackCover.hide)
GLib.idle_add(self.shuffBut.hide)
GLib.idle_add(self.drop_but.hide)
GLib.idle_add(self.subcheck.show)
GLib.idle_add(self.karaokeBut.set_from_icon_name, self.switchDict[btn][1], Gtk.IconSize.BUTTON)
self.useMode = self.switchDict[btn][2]
else:
GLib.idle_add(self.exBot.hide)
GLib.idle_add(self.subcheck.hide)
GLib.idle_add(self.drop_but.hide)
self.needSub = False
GLib.idle_add(self.subcheck.set_state, False)
GLib.idle_add(self.switchDict[btn][3].set_active, False)
GLib.idle_add(self.switchDict[btn][4].set_active, False)
elif self.mainStack.get_visible_child() == self.switchDict[btn][0]: GLib.idle_add(button.set_active, True)
@profile
def cleaner(self, lis):
if lis == []: pass
else: [i.destroy() for i in lis]
@profile
def neo_playlist_gen(self, name="", src=0, dst=0):
if name == 'shuffle':
if self.playlistPlayer == True:
playlistLoc = sample(self.playlist, len(self.playlist))
self.playlist, self.tnum = playlistLoc, 0
self.neo_playlist_gen()
self.on_next('clickMode')
elif name == "rename":
srcBox = self.supBox.get_children()[src]
self.supBox.reorder_child(srcBox, dst)
tmpList = self.supBox.get_children()
for i in range(len(self.playlist)):
tmpList[i].set_name(f"trackbox_{i}")
if self.tnum == src: self.tnum = dst
elif src > dst: self.tnum += 1
elif src < dst: self.tnum -= 1
self.themer(self.roundSpin.get_value(), self.tnum)
else:
self.cleaner(self.playlistBox.get_children())
self.supBox = Gtk.Box.new(1, 0)
self.supBox.set_can_focus(False)
for i, item in enumerate(self.playlist):
trBox = TrackBox(item["title"].replace("&", "&"), item["artist"], i, item["year"], item["length"], item["album"])
trBox.connect("button_release_event", self.highlight)
trBox.connect('drag-begin', self.pause)
trBox.connect('drag-drop', self._sig_drag_drop)
trBox.connect('drag-end', self._sig_drag_end)
self.supBox.pack_start(trBox, False, False, 0)
yetScroll = Gtk.ScrolledWindow()
yetScroll.set_can_focus(False)
yetScroll.set_vexpand(True)
yetScroll.set_hexpand(True)
yetScroll.set_margin_end(10)
yetScroll.add(self.supBox)
self.playlistBox.pack_end(yetScroll, True, True, 0)
yetScroll.show_all()
self.playlistPlayer = True
GLib.idle_add(self.drop_but.show)
@profile
def metas(self, location, extrapath, misc=False):
f = MediaFile(location)
title, artist, album, year, length = f.title, f.artist, f.album, f.year, str(timedelta(seconds=round(f.length))).replace("0:", "")
if not title: title = os.path.splitext(extrapath)[0]
if not artist: artist = "Unknown"
if not album: album = "Unknown"
if not year: year = 0
itmp = {"uri" : location, "title" : title, "artist" : artist, "year" : year, "album" : album, "length" : length}
if misc == True:
if itmp not in self.playlist: self.pltmp.append(itmp)
else: self.pltmp.append(itmp)
@profile
def loader(self, path, misc=False):
self.pltmp = []
if self.clickedE:
self.metas(self.clickedE, self.clickedE.split("/")[-1])
else:
pltmpin = os.listdir(path)
for i in pltmpin:
ityp = os.path.splitext(i)[1]
if ityp in self.supportedList:
self.metas(f"{path}/{i}", i, misc)
if misc == False: self.playlist = self.pltmp
else: [self.playlist.append(item) for item in self.pltmp]
GLib.idle_add(self.neo_playlist_gen)
@profile
def on_openFolderBut_clicked(self, *_):
if self.playing == True: self.pause()
self.fcconstructer("Please choose a folder", Gtk.FileChooserAction.SELECT_FOLDER, "Music") if self.useMode == "audio" else self.fcconstructer("Please choose a video file", Gtk.FileChooserAction.OPEN, "Videos")
@profile
def fcconstructer (self, title, action, folder):
filechooserdialog = Gtk.FileChooserDialog(title=title, parent=self.window, action=action)
if folder == "Videos":
filterr = Gtk.FileFilter()
filterr.set_name("Video files")
filterr.add_mime_type("video/*")
filechooserdialog.add_filter(filterr)
filechooserdialog.add_button("_Cancel", Gtk.ResponseType.CANCEL)
filechooserdialog.add_button("_Open", Gtk.ResponseType.OK)
filechooserdialog.set_default_response(Gtk.ResponseType.OK)
filechooserdialog.set_current_folder(f"/home/{user}/{folder}")
response = filechooserdialog.run()
if response == Gtk.ResponseType.OK:
if folder == "Music":
self.folderPath = filechooserdialog.get_uri().replace("file://", "")
print("Folder selected: " + self.folderPath)
if os.path.isfile(f"{self.folderPath}/.saved.order"):
f = open(f"{self.folderPath}/.saved.order", "r")
self.playlist = json.loads(f.read())
f.close()
for item in self.playlist[:]:
if os.path.isfile(f"{item['uri']}") == False: self.playlist.remove(item)
GLib.idle_add(self.neo_playlist_gen)
else:
d_pl = futures.ThreadPoolExecutor(max_workers=4)
d_pl.submit(self.loader, self.folderPath)
else:
videoPath = filechooserdialog.get_filename()
print("File selected: " + videoPath)
self.on_playBut_clicked(videoPath)
elif response == Gtk.ResponseType.CANCEL: print("Cancel clicked")
filechooserdialog.destroy()
@profile
def on_save(self, *_):
f = MediaFile(self.editingFile)
f.year, f.artist, f.album, f.title, newCover = self.yrEnt.get_value_as_int(), self.arEnt.get_text(), self.alEnt.get_text(), self.tiEnt.get_text(), self.iChoser.get_filename()
try:
if os.path.isfile(newCover):
tf = open(newCover, "rb")
binary = tf.read()
tf.close()
f.art = binary
except: pass
f.save()
self.playlist[self.ednum]["year"] = self.yrEnt.get_value_as_int()
self.playlist[self.ednum]["artist"] = self.arEnt.get_text()
self.playlist[self.ednum]["album"] = self.alEnt.get_text()
self.playlist[self.ednum]["title"] = self.tiEnt.get_text()
self.sub2_hide("xy")
self.neo_playlist_gen()
@profile
def sub2_hide(self, *_):
self.sub2.hide()
return True
@profile
def ed_cur(self, *_):
self.editingFile = self.playlist[self.ednum]["uri"].replace("file://", "")
self.yrEnt.set_value(self.playlist[self.ednum]["year"])
self.arEnt.set_text(self.playlist[self.ednum]["artist"])
self.alEnt.set_text(self.playlist[self.ednum]["album"])
self.tiEnt.set_text(self.playlist[self.ednum]["title"])
self.sub2.set_title(f"Edit metadata for {self.editingFile.split('/')[-1]}")
self.sub2.show_all()
@profile
def mouse_click0(self, _, event):
if event.type == Gdk.EventType._2BUTTON_PRESS:
if self.useMode == "video": self.on_karaoke_activate(0)
elif event.type == Gdk.EventType.BUTTON_PRESS: self.on_playBut_clicked(0)
@profile
def del_cur(self, *_):
self.playlist.remove(self.playlist[self.ednum])
self.neo_playlist_gen()
if self.tnum == self.ednum: self.play()
@profile
def on_next(self, button):
if self.nowIn == self.useMode or button == "clickMode":
if self.nowIn == "audio" or button == "clickMode":
if button != "clickMode":
self.tnum += 1
if self.tnum >= len(self.playlist): self.tnum = 0
try:
self.stop()
except: print("No playbin yet to stop.")
self.play()
if self.sub.get_visible(): self.on_karaoke_activate("xy")
elif self.nowIn == "video":
seek_time_secs = self.player.query_position(Gst.Format.TIME)[1] + 10 * Gst.SECOND
self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seek_time_secs)
@profile
def load_cover(self):
binary = MediaFile(self.url.replace('file://', '')).art
if not binary: tmpLoc = "icons/track.png"
else:
tmpLoc = "/tmp/cacheCover.jpg"
f = open(tmpLoc, "wb")
f.write(binary)
f.close()
coverBuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(tmpLoc, 80, 80, True)
GLib.idle_add(self.trackCover.set_from_pixbuf, coverBuf)
@profile
def on_prev(self, *_):
if self.nowIn == self.useMode:
if self.nowIn == "audio":
self.tnum -= 1
if self.tnum < 0: self.tnum = len(self.playlist)-1
try:
self.pause()
self.stop()
except: print("No playbin yet to stop.")
self.play()
elif self.nowIn == "video":
seek_time_secs = self.player.query_position(Gst.Format.TIME)[1] - 10 * Gst.SECOND
self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seek_time_secs)
@profile
def stop(self):
print("Stop")
self.player.set_state(Gst.State.PAUSED)
if self.nowIn == "audio": self.audioPipe = self.player
elif self.nowIn == "video": self.videoPipe = self.player
self.res = False
self.playing = False
self.label.set_text("0:00")
self.label_end.set_text("0:00")
GLib.idle_add(self.plaicon.set_from_icon_name, "media-playback-start", Gtk.IconSize.BUTTON)
self.slider.handler_block(self.slider_handler_id)
self.slider.set_value(0)
self.slider.handler_unblock(self.slider_handler_id)
self.player.set_state(Gst.State.NULL)
@profile
def pause(self, *_):
print("Pause")
self.playing = False
try:
GLib.idle_add(self.plaicon.set_from_icon_name, "media-playback-start", Gtk.IconSize.BUTTON)
self.player.set_state(Gst.State.PAUSED)
except: print("Pause exception")
@profile
def resume(self):
print("Resume")
self.playing = True
self.player.set_state(Gst.State.PLAYING)
GLib.idle_add(self.plaicon.set_from_icon_name, "media-playback-pause", Gtk.IconSize.BUTTON)
GLib.timeout_add(50, self.updateSlider)
@profile
def on_slider_seek(self, *_):
if self.useMode == self.nowIn:
seek_time_secs = self.slider.get_value()
if seek_time_secs < self.position:
self.seekBack = True
print('back')
self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seek_time_secs * Gst.SECOND)
@profile
def updateSlider(self):
if(self.playing == False): return False # cancel timeout
try:
duration_nanosecs = self.player.query_duration(Gst.Format.TIME)[1]
position_nanosecs = self.player.query_position(Gst.Format.TIME)[1]
if duration_nanosecs == -1: return True
# block seek handler so we don't seek when we set_value()
self.slider.handler_block(self.slider_handler_id)
duration = float(duration_nanosecs) / Gst.SECOND
self.position = float(position_nanosecs) / Gst.SECOND
remaining = float(duration_nanosecs - position_nanosecs) / Gst.SECOND
self.slider.set_range(0, duration)
self.slider.set_value(self.position)
fvalue, svalue = str(timedelta(seconds=round(self.position))), str(timedelta(seconds=int(remaining)))
self.label.set_text(fvalue)
self.label_end.set_text(svalue)
self.slider.handler_unblock(self.slider_handler_id)
except Exception as e:
print (f'W: {e}')
pass
return True
@profile
def on_shuffBut_clicked(self, *_): self.neo_playlist_gen(name='shuffle')
@profile
def nosub_hide(self, *_):
self.nosub.hide()
return True
@profile
def on_act_sub(self, _, state):
if state == True and self.nowIn == "video":
filename = self.url.replace("file://", "").split("/")[-1]
tmpdbnow = os.listdir(self.url.replace("file://", "").replace(filename, ""))
if os.path.splitext(filename)[0]+".srt" in tmpdbnow:
print("Subtitle found!")
srfile = os.path.splitext(self.url.replace("file://", ""))[0]+".srt"
print(srfile)
with open (srfile, 'r') as subfile:
presub = subfile.read()
subtitle_gen = srt.parse(presub)
subtitle = list(subtitle_gen)
self.needSub = True
subs = futures.ThreadPoolExecutor(max_workers=2)
subs.submit(self.subShow, subtitle)
else:
self.nosub.set_title("Subtitle file not found")
self.nosub.show_all()
GLib.idle_add(self.subcheck.set_state, False)
self.pause()
else: self.needSub = False
@profile
def play(self, misc=""):
if self.clickedE:
self.url, self.nowIn = "file://"+self.clickedE, self.useMode
if self.useMode == "audio": self.player = self.audioPipe
else: self.player = self.videoPipe
elif "/" in misc:
self.url, self.nowIn, self.player = "file://"+misc, "video", self.videoPipe
GLib.idle_add(self.subcheck.set_state, False)
elif misc == "continue":
if self.useMode == "audio":
if self.audioPipe.get_state(1)[1] == Gst.State.NULL: return
self.player, self.nowIn = self.audioPipe, "audio"
else:
if self.videoPipe.get_state(1)[1] == Gst.State.NULL: return
self.player, self.nowIn = self.videoPipe, "video"
else:
try: self.url, self.nowIn, self.player = "file://"+self.playlist[self.tnum]["uri"], "audio", self.audioPipe
except: return
print("Play")
self.res, self.playing, self.position = True, True, 0
if self.useMode == "audio": self.themer(self.roundSpin.get_value(), self.tnum)
if misc != "continue":
self.player.set_state(Gst.State.NULL)
self.player.set_property("uri", self.url)
self.player.set_state(Gst.State.PLAYING)
GLib.idle_add(self.header.set_subtitle, self.url.replace("file://", "").split("/")[-1])
GLib.idle_add(self.plaicon.set_from_icon_name, "media-playback-pause", Gtk.IconSize.BUTTON)
GLib.timeout_add(50, self.updateSlider)
if self.useMode == "audio" and misc != "continue":
ld_cov = futures.ThreadPoolExecutor(max_workers=1)
ld_cov.submit(self.load_cover)
@profile
def diabuilder (self, text, title, mtype, buts):
x, y = self.window.get_position()
sx, sy = self.window.get_size()
dialogWindow = Gtk.MessageDialog(parent=self.window, modal=True, destroy_with_parent=True, message_type=mtype, buttons=buts, text=text, title=title)
dsx, dsy = dialogWindow.get_size()
dialogWindow.move(x+((sx-dsx)/2), y+((sy-dsy)/2))
dialogWindow.show_all()
dialogWindow.run()
dialogWindow.destroy()
@profile
def on_playBut_clicked(self, button):
if self.nowIn == self.useMode or self.nowIn == "" or "/" in button:
if not self.playing:
if not self.res or "/" in str(button): self.play(button)
else: self.resume()
else: self.pause()
else: self.play("continue")
@profile
def on_main_delete_event(self, window, e):
try: self.mainloop.quit()
except: pass
self.force, self.stopKar, self.needSub, self.hardReset = True, True, False, True
raise SystemExit
@profile
def listener(self):
try:
APP_ID = "hbud"
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.Bus(dbus.Bus.TYPE_SESSION)
bus_object = bus.get_object('org.gnome.SettingsDaemon', '/org/gnome/SettingsDaemon/MediaKeys')
dbus_interface='org.gnome.SettingsDaemon.MediaKeys'
bus_object.GrabMediaPlayerKeys(APP_ID, 0, dbus_interface=dbus_interface)
bus_object.connect_to_signal('MediaPlayerKeyPressed', self.on_media)
self.mainloop = GLib.MainLoop()
self.mainloop.run()
except:
print("I: Not compatible with Gnome MediaKeys daemon")
@profile
def on_media(self, app, action):
print(app, action)
if app == "hbud" and self.url:
if action == "Next": self.on_next("xy")
elif action == "Previous": self.on_prev("xy")
elif not self.playing: self.resume()
elif self.playing: self.pause()
@profile
def _sig_drag_drop(self, widget, *_): self.dst = int(widget.get_name().replace("trackbox_", ""))
@profile
def _sig_drag_end(self, widget, _): self.reorderer(int(widget.get_name().replace("trackbox_", "")), self.dst)
@profile
def reorderer(self, src, dst):
playlistLoc, cutList = self.playlist, []
if dst < src:
[cutList.append(playlistLoc[i]) for i in range(dst, src+1)]
rby, corrector = 1, dst
elif dst > src:
[cutList.append(playlistLoc[i]) for i in range(src, dst+1)]
rby, corrector = -1, src
cutList = deque(cutList)
cutList.rotate(rby)
cutList = list(cutList)
for i in range(len(cutList)):
self.playlist[i+corrector] = cutList[i]
GLib.idle_add(self.neo_playlist_gen, "rename", src, dst)
@profile
def on_key(self, _, key):
# Add on_key as key_press signal to the ui file - main window preferably
# print(key.keyval)
try:
if key.keyval == 32 and self.url: self.on_playBut_clicked(0) # Space
elif key.keyval == 65307 or key.keyval == 65480:
if self.useMode == "video": self.on_karaoke_activate(0) # ESC and F11
elif key.keyval == 65363: self.on_next("") # Right
elif key.keyval == 65361: self.on_prev("") # Left
elif key.keyval == 65535 and self.useMode == "audio": # Delete
self.ednum = self.tnum
self.del_cur()
elif key.keyval == 65362 and self.useMode == "audio": # Up
self.reorderer(self.tnum, self.tnum-1)
elif key.keyval == 65364 and self.useMode == "audio": # Down
self.reorderer(self.tnum, self.tnum+1)
except: pass
@profile
def on_message(self, _, message):
t = message.type
if t == Gst.MessageType.EOS and self.nowIn == "audio": self.on_next("xy")
elif t == Gst.MessageType.ERROR:
self.player.set_state(Gst.State.NULL)
err, debug = message.parse_error()
print (f"Error: {err}", debug)
@profile
def _on_size_allocated(self, *_):
sleep(0.01)
x, y = self.sub.get_size()
self.size, self.size2 = 50*x, 21.4285714*x
@profile
def _on_size_allocated0(self, *_):
sleep(0.01)
if self.needSub == True:
x, y = self.window.get_size()
self.size3, self.size4 = self.sSize*y, float(f"0.0{self.sMarg}")*y
@profile
def get_lyrics(self, title, artist):
self.DAPI.title, self.DAPI.artist = title, artist
try: result = self.DAPI.getLyrics()
except: result = 0
return result
@profile
def on_karaoke_activate(self, *_):
if self.nowIn == "audio":
if self.playing == True or self.res == True:
print('Karaoke')
self.stopKar = False
track = self.playlist[self.tnum]["title"]
try:
artist = self.playlist[self.tnum]["artist"].split("/")[0]
if artist == "AC": artist = self.playlist[self.tnum]["artist"]
except: artist = self.playlist[self.tnum]["artist"]
dbnow = []
if self.clickedE:
folPathClick = self.clickedE.replace(self.clickedE.split("/")[-1], "")
tmpdbnow = os.listdir(folPathClick)
else: tmpdbnow = os.listdir(self.folderPath)
for i in tmpdbnow:
if ".srt" in i or ".txt" in i:
x = i
if self.clickedE: dbnow.append(f"{folPathClick}{x}")
else: dbnow.append(f"{self.folderPath}/{x}")
self.sub.set_title(f'{track} - {artist}')
tmp = os.path.splitext(self.playlist[self.tnum]["uri"])[0]
if f"{tmp}.srt" not in dbnow:
if f"{tmp}.txt" in dbnow:
f = open(f"{tmp}.txt", "r")
lyric = f.read()
f.close()
self.lyrLab.set_label(lyric)
self.subStack.set_visible_child(self.lyrmode)
self.sub.show_all()
else:
lyric = self.get_lyrics(track, artist)
if lyric == 0:
self.diabuilder('Can not get lyrics for the current track. Please place the synced .srt file or the raw .txt file alongside the audio file, with the same name as the audio file.', "Information", Gtk.MessageType.INFO, Gtk.ButtonsType.OK)
else:
f = open(f"{tmp}.txt", "w+")
f.write(lyric)
f.close()
self.lyrLab.set_label(lyric)
self.subStack.set_visible_child(self.lyrmode)
self.sub.show_all()
else:
print("FOUND")
self.start_karaoke(f"{tmp}.srt")
self.subStack.set_visible_child(self.karmode)
self.sub.show_all()
elif self.useMode == "video":
if self.fulle == False:
GLib.idle_add(self.karaokeBut.set_from_icon_name, "view-restore", Gtk.IconSize.BUTTON)
self.window.fullscreen()
ld_clock = futures.ThreadPoolExecutor(max_workers=1)
ld_clock.submit(self.clock)
else:
GLib.idle_add(self.karaokeBut.set_from_icon_name, "view-fullscreen", Gtk.IconSize.BUTTON)
self.resete, self.keepReset = False, False
self.window.unfullscreen()
self.mage()
@profile
def mouse_enter(self, *_):
if self.fulle == True: self.keepReset = True
@profile
def mouse_leave(self, *_):
if self.fulle == True: self.keepReset = False
@profile
def mage(self):
GLib.idle_add(self.exBot.show)
cursor = Gdk.Cursor.new_from_name(self.window.get_display(), 'default')
self.window.get_window().set_cursor(cursor)
@profile
def mouse_moving(self, *_):
if self.fulle == True:
self.resete = True
if self.exBot.get_visible() == False:
self.mage()
ld_clock = futures.ThreadPoolExecutor(max_workers=1)
ld_clock.submit(self.clock)
@profile
def clock(self):
start = time()
while time() - start < 2:
sleep(0.00001)
if self.hardReset == True: return
if self.keepReset == True: start = time()
elif self.resete == True: start, self.resete = time(), False
if self.fulle == True:
GLib.idle_add(self.exBot.hide)
cursor = Gdk.Cursor.new_for_display(self.window.get_display(), Gdk.CursorType.BLANK_CURSOR)
self.window.get_window().set_cursor(cursor)
@profile
def on_state_change(self, _, event):
if event.changed_mask & Gdk.WindowState.FULLSCREEN:
self.fulle = bool(event.new_window_state & Gdk.WindowState.FULLSCREEN)
print(self.fulle)
@profile
def subShow(self, subtitle):
while self.needSub == True:
sleep(0.001)
for line in subtitle:
if self.position >= line.start.total_seconds() and self.position <= line.end.total_seconds():
if self.bge == True: GLib.idle_add(self.theTitle.set_markup, f"{line.content}")
else: GLib.idle_add(self.theTitle.set_markup, f"{line.content}")
self.theTitle.set_margin_bottom(self.size4)
GLib.idle_add(self.theTitle.show)
while self.needSub == True and self.position <= line.end.total_seconds() and self.position >= line.start.total_seconds():
sleep(0.001)
pass
GLib.idle_add(self.theTitle.hide)
GLib.idle_add(self.theTitle.set_label, "")
@profile
def start_karaoke(self, sfile):
with open (sfile, "r") as subfile: presub = subfile.read()
subtitle_gen = srt.parse(presub)
subtitle, lyrs = list(subtitle_gen), futures.ThreadPoolExecutor(max_workers=2)
lyrs.submit(self.slideShow, subtitle)
@profile
def slideShow(self, subtitle):
self.lenlist = len(subtitle)-1
while not self.stopKar:
self.line1, self.line2, self.line3, self.buffer = [], [], [], []
self.hav1, self.hav2, self.hav3, self.where = False, False, False, -1
for word in subtitle:
if '#' in word.content:
self.buffer.append(word)
if not self.hav1: self.to1()
elif not self.hav2: self.to2()
else: self.to3()
else: self.buffer.append(word)
if self.stopKar or self.seekBack: break
self.where += 1
if not self.seekBack:
self.to2()
self.to1()
self.line2 = []
self.sync()
self.stopKar = True
else: self.seekBack = False
@profile
def to1(self):
if self.hav2: self.line1 = self.line2
else:
self.line1 = self.buffer
self.buffer = []
self.hav1 = True
@profile
def to2(self):
if self.where+1 <= self.lenlist:
if self.hav3: self.line2 = self.line3
else:
self.line2 = self.buffer
self.buffer = []
self.hav2 = True
else:
if self.hav1 and not self.hav3: self.to1()
self.line2 = self.line3
self.line3 = []
self.sync()
@profile
def to3(self):
if self.where+2 <= self.lenlist:
if self.hav1 and self.hav3: self.to1()
if self.hav2 and self.hav3: self.to2()
self.line3 = self.buffer
self.buffer, self.hav3 = [], True
else:
if self.hav1: self.to1()
if self.hav2: self.to2()
self.line3 = self.buffer
self.buffer, self.hav3 = [], False
self.sync()
@profile
def sync(self):
simpl2, simpl3 = "", ""
if self.line2 != []:
for z in self.line2:
if self.stopKar or self.seekBack: break
simpl2 += f"{z.content.replace('#', '')} "
else: simpl2 = ""
if self.line3 != []:
for z in self.line3:
if self.stopKar or self.seekBack: break
simpl3 += f"{z.content.replace('#', '')} "
else: simpl3 = ""
GLib.idle_add(self.label2.set_markup, f"{simpl2}")
GLib.idle_add(self.label3.set_markup, f"{simpl3}")
done, tmpline, first, tl1, it = "", self.line1[:], True, self.line1, 1
tl1.insert(0, "")
maxit = len(tl1)-1
for xy in tl1:
if self.stopKar or self.seekBack: break
if first: first = False
else: tmpline = tmpline[1:]
leftover = ""
for y in tmpline:
if self.stopKar or self.seekBack: break
leftover += f"{y.content.replace('#', '')} "
try: GLib.idle_add(self.label1.set_markup, f"{done} {xy.content.replace('#', '')} {leftover}")
except: GLib.idle_add(self.label1.set_markup, f"{done} {xy} {leftover}")
while not self.stopKar:
sleep(0.01)
if it > maxit:
if self.position >= xy.end.total_seconds()-0.05 and self.position >= 0.5: break
else:
xz = tl1[it]
if self.position >= xz.start.total_seconds()-0.1 and self.position >= 0.5: break
if self.seekBack: break
it += 1
try:
if done == "": done += f"{xy.content.replace('#', '')}"
else: done += f" {xy.content.replace('#', '')}"
except: pass
@profile
def config_write(self, *_):
self.darke, self.bge, self.color = self.dark_switch.get_state(), self.bg_switch.get_state(), self.colorer.get_rgba().to_string()
tmp1, tmp2, tmp3 = int(self.subSpin.get_value()), int(self.subMarSpin.get_value()), int(self.roundSpin.get_value())
parser.set('subtitles', 'margin', str(tmp2))
parser.set('subtitles', 'size', str(tmp1))
parser.set('subtitles', 'bg', str(self.bge))
parser.set('gui', 'rounded', str(tmp3))
parser.set('gui', 'dark', str(self.darke))
parser.set('gui', 'color', self.color)
file = open(confP, "w+")
parser.write(file)
file.close()
self.settings.set_property("gtk-application-prefer-dark-theme", self.darke)
self.themer(str(tmp3), self.tnum)
self.sSize, self.sMarg = tmp1, tmp2
@profile
def on_hide(self, *_):
self.stopKar = True
self.sub.hide()
return True
if __name__ == "__main__":
user = os.popen("who|awk '{print $1}'r").read().rstrip().split('\n')[0]
parser, confP = ConfigParser(), f"/home/{user}/.config/hbud.ini"
if os.path.isfile(confP): parser.read(confP)
else:
os.system(f"touch {confP}")
parser.add_section('subtitles')
parser.set('subtitles', 'margin', str(66))
parser.set('subtitles', 'size', str(30))
parser.set('subtitles', 'bg', "False")
parser.add_section('gui')
parser.set('gui', 'rounded', "10")
parser.set('gui', 'dark', "False")
parser.set('gui', 'color', "rgb(17, 148, 156)")
file = open(confP, "w+")
parser.write(file)
file.close()
sSize, sMarg, bg = parser.get('subtitles', 'size'), parser.get('subtitles', 'margin'), parser.get('subtitles', 'bg')
rounded, dark, color = parser.get('gui', 'rounded'), parser.get('gui', 'dark'), parser.get('gui', 'color')
Gst.init(None)
app = GUI()
Gtk.main()