class GPlayer: def __init__(self): print("Init GStreamer...") # This is used to keep track of time between callbacks. self.player_timer = Timer() # Store the track object that is currently playing self.loaded_track = None # This is used to keep note of what state of playing we should be in self.play_state = 0 # 0 is stopped, 1 is playing, 2 is paused # Initiate GSteamer Gst.init([]) self.mainloop = GLib.MainLoop() # Populate list of output devices with defaults outputs = {} devices = [ "PulseAudio", "ALSA", "JACK", ] if tauon.snap_mode: # Snap permissions don't support these by default devices.remove("JACK") devices.remove("ALSA") # Get list of available audio device self.dm = Gst.DeviceMonitor() self.dm.start() for device in self.dm.get_devices(): if device.get_device_class() == "Audio/Sink": element = device.create_element(None) type_name = element.get_factory().get_name() if hasattr(element.props, "device"): device_name = element.props.device display_name = device.get_display_name() # This is used by the UI to present list of options to the user in audio settings outputs[display_name] = (type_name, device_name) devices.append(display_name) # dm.stop() # Causes a segfault sometimes pctl.gst_outputs = outputs pctl.gst_devices = devices # Create main "playbin" pipeline for playback self.playbin = Gst.ElementFactory.make("playbin", "player") # Create custom output bin from user preferences if not prefs.gst_use_custom_output: prefs.gst_output = prefs.gen_gst_out() self._output = Gst.parse_bin_from_description( prefs.gst_output, ghost_unlinked_pads=True) # Create a bin for the audio pipeline self._sink = Gst.ElementFactory.make("bin", "sink") self._sink.add(self._output) # Spectrum ------------------------- # Cant seem to figure out how to process these magnitudes in a way that looks good # self.spectrum = Gst.ElementFactory.make("spectrum", "spectrum") # self.spectrum.set_property('bands', 20) # self.spectrum.set_property('interval', 10000000) # self.spectrum.set_property('threshold', -100) # self.spectrum.set_property('post-messages', True) # self.spectrum.set_property('message-magnitude', True) # # self.playbin.set_property('audio-filter', self.spectrum) # # ------------------------------------ # # Level Meter ------------------------- self.level = Gst.ElementFactory.make("level", "level") self.level.set_property('interval', 20000000) self.playbin.set_property('audio-filter', self.level) # # ------------------------------------ self._eq = Gst.ElementFactory.make("equalizer-nbands", "eq") self._vol = Gst.ElementFactory.make("volume", "volume") self._sink.add(self._eq) self._sink.add(self._vol) self._eq.link(self._vol) self._vol.link(self._output) self._eq.set_property("num-bands", 10 + 2) # Set the equalizer based on user preferences # Using workaround for "inverted slider" bug. # Thanks to Lollypop and Clementine for discovering this. # Ref https://github.com/Taiko2k/TauonMusicBox/issues/414 for i in range(10 + 2): band = self._eq.get_child_by_index(i) band.set_property("freq", 0) band.set_property("bandwidth", 0) band.set_property("gain", 0) self.set_eq() # if prefs.use_eq: # self._eq.set_property("band" + str(i), level * -1) # else: # self._eq.set_property("band" + str(i), 0.0) # Set up sink pad for the intermediate bin via the # first element (volume) ghost = Gst.GhostPad.new("sink", self._eq.get_static_pad("sink")) self._sink.add_pad(ghost) # Connect the playback bin to to the intermediate bin sink pad self.playbin.set_property("audio-sink", self._sink) # The pipeline should look something like this - # (player) -> [(eq) -> (volume) -> (output)] # Create controller for pause/resume volume fading self.c_source = GstController.InterpolationControlSource() self.c_source.set_property('mode', GstController.InterpolationMode.LINEAR) self.c_binding = GstController.DirectControlBinding.new( self._vol, "volume", self.c_source) self._vol.add_control_binding(self.c_binding) # Set callback for the main callback loop GLib.timeout_add(50, self.main_callback) self.playbin.connect("about-to-finish", self.about_to_finish) # Not used # Setup bus and select what types of messages we want to listen for bus = self.playbin.get_bus() bus.add_signal_watch() bus.connect('message::element', self.on_message) bus.connect('message::buffering', self.on_message) bus.connect('message::error', self.on_message) bus.connect('message::tag', self.on_message) bus.connect('message::warning', self.on_message) # bus.connect('message::eos', self.on_message) # Variables used with network downloading self.temp_id = "a" self.url = None self.dl_ready = True self.using_cache = False self.temp_path = "" # Full path + filename # self.level_train = [] self.seek_timer = Timer() self.seek_timer.force_set(10) self.buffering = False # Other self.end_timer = Timer() # Start GLib mainloop self.mainloop.run() def set_eq(self): last = 0 for i, level in enumerate(prefs.eq): band = self._eq.get_child_by_index(i + 1) if prefs.use_eq: band = self._eq.get_child_by_index(i + 1) freq = 31.25 * (2**i) band.set_property("freq", freq) band.set_property("bandwidth", freq - last) last = freq band.set_property("gain", level * -1) else: band.set_property("freq", 0) band.set_property("bandwidth", 0) band.set_property("gain", 0) def about_to_finish(self, bin): self.end_timer.set() def on_message(self, bus, msg): struct = msg.get_structure() #print(struct.get_name()) #print(struct.to_string()) name = struct.get_name() if name == "GstMessageError": if "_is_dead" in struct.to_string(): # Looks like PulseAudio was reset. Need to restart playback. self.playbin.set_state(Gst.State.NULL) if tauon.stream_proxy.download_running: tauon.stream_proxy.stop() else: self.playbin.set_state(Gst.State.PLAYING) tries = 0 while tries < 10: time.sleep(0.03) r = self.playbin.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, (pctl.start_time_target + pctl.playing_time) * Gst.SECOND) if r: break tries += 1 if self.play_state == 3 and name == "GstMessageTag": if not tauon.radiobox.parse_vorbis_okay(): return data = struct.get_value("taglist").get_string("title") data2 = struct.get_value("taglist").get_string("artist") data3 = struct.get_value("taglist").get_string("year") data4 = struct.get_value("taglist").get_string("album") # print(struct.to_string()) if data[0]: pctl.tag_meta = "" line = "" line = data[1] if data2[0]: line = data2[1] + " - " + line pctl.found_tags = {} pctl.found_tags["title"] = data[1] if data2[0]: pctl.found_tags["artist"] = data2[1] if data3[0]: pctl.found_tags["year"] = str(data3[1]) if data4[0]: pctl.found_tags["album"] = data4[1] pctl.tag_meta = line print("Found tag: " + line) elif name == "GstMessageError": if "Connection" in struct.get_value("debug"): gui.show_message("Connection error", mode="info") elif name == 'GstMessageBuffering': if pctl.playing_state == 3: buff_percent = struct.get_value("buffer-percent") if buff_percent == 0 and (self.play_state == 1 or self.play_state == 3): self.playbin.set_state(Gst.State.PAUSED) self.buffering = True print("Buffering...") elif self.buffering and buff_percent == 100 and ( self.play_state == 1 or self.play_state == 3): self.playbin.set_state(Gst.State.PLAYING) self.buffering = False print("Buffered") if gui.vis == 1 and name == 'level': # print(struct.to_string()) data = struct.get_value("peak") ts = struct.get_value("timestamp") # print(data) l = (10**(data[0] / 20)) * 11.6 if len(data) == 1: r = l else: r = (10**(data[1] / 20)) * 11.6 td = (ts / 1000000000) - (self.playbin.query_position( Gst.Format.TIME)[1] / Gst.SECOND) t = time.time() rt = t + td if td > 0: for item in tauon.level_train: if rt < item[0]: tauon.level_train.clear() # print("FF") break tauon.level_train.append((rt, l, r)) # if name == 'spectrum': # struct_str = struct.to_string() # magnitude_str = re.match(r'.*magnitude=\(float\){(.*)}.*', struct_str) # if magnitude_str: # magnitude = map(float, magnitude_str.group(1).split(',')) # # l = list(magnitude) # k = [] # #print(l) # for a in l[0:20]: # #v = ?? # k.append() # print(k) # gui.spec = k # #print(k) # gui.level_update = True # # return True def check_duration(self): # This function is to be called when loading a track to query for a duration of track # in case the tagger failed to calculate a length for the track when imported. # Get current playing track object from player current_track = pctl.playing_object() if current_track is not None and ( current_track.length < 1 or current_track.file_ext.lower() in tauon.mod_formats): time.sleep(0.25) result = self.playbin.query_duration(Gst.Format.TIME) if result[0] is True: current_track.length = result[1] / Gst.SECOND pctl.playing_length = current_track.length gui.pl_update += 1 else: # still loading? I guess we wait and try again. time.sleep(1.5) result = self.playbin.query_duration(Gst.Format.TIME) if result[0] is True: current_track.length = result[1] / Gst.SECOND def download_part(self, url, target, params, id): # GStreamer can't seek some types of HTTP sources. # # To work around this, when a seek is requested by the user, this # function facilitates the download of the URL in whole, then loaded # into GStreamer as a complete file to provide at least some manner of seeking # ability for the user. (User must wait for full download) # # (Koel and Airsonic MP3 sources are exempt from this as seeking does work with them) # # A better solution might be to download file externally then feed the audio data # into GStreamer as it downloads. This still would have the issue that the whole file # must have been downloaded before a seek could begin. # # With the old BASS backend, this was done with the file on disk being constantly # appended to. Unfortunately GStreamer doesn't support playing files in this manner. try: part = requests.get(url, stream=True, params=params) except: gui.show_message("Could not connect to server", mode="error") self.dl_ready = True return bitrate = 0 a = 0 z = 0 # print(target) f = open(target, "wb") for chunk in part.iter_content(chunk_size=1024): if chunk: # filter out keep-alive new chunks a += 1 # if a == 300: # kilobyes~ # self.dl_ready = True if id != self.id: part.close() f.close() os.remove(target) return # break f.write(chunk) # Periodically update download the progress indicator z += 1 if id == self.id: if z == 60: z = 0 if bitrate == 0: audio = auto.File(target) bitrate = audio.bitrate if bitrate > 0: gui.update += 1 pctl.download_time = a * 1024 / (bitrate / 8) / 1000 f.close() pctl.download_time = -1 self.dl_ready = True def main_callback(self): if not pctl.playerCommandReady and pctl.playing_state == 0 and not tauon.spot_ctl.playing and not tauon.spot_ctl.coasting: # tauon.tm.player_lock.acquire() # Blocking the GLib thread blocks tray processing GLib.timeout_add(50, self.main_callback) return if gui.vis == 1: if pctl.playing_state == 1: gui.level_update = True # This is the main callback function to be triggered continuously as long as application is running if self.play_state == 1 and pctl.playing_time > 1 and not pctl.playerCommandReady: pctl.test_progress( ) # This function triggers an advance if we are near end of track success, state, pending = self.playbin.get_state(3 * Gst.SECOND) if state != Gst.State.PLAYING: time.sleep(0.5) print("Stall...") if self.play_state == 3: pctl.radio_progress() if not pctl.playerCommandReady: pctl.spot_test_progress() if pctl.playerCommandReady: command = pctl.playerCommand pctl.playerCommand = "" pctl.playerCommandReady = False # print("RUN COMMAND") # print(command) # Here we process commands from the main thread/module # Possible commands: # open: Start playback of a file # Path given by pctl.target_open at position pctl.start_time_target + pctl.jump_time # stop: Stop playback (Implies release file) # runstop: Stop playback but let finish if we are near the end of the file # pauseon: Pause playback (be ready to resume) # pauseoff: Resume playback if paused # volume: Set to the volume specified by pctl.player_volume (0 to 100) # seek: Seek to position given by pctl.new_time + pctl.start_time (don't resume playback if paused) # url: Start playback of a shoutcast/icecast stream. URL specified by pctl.url # todo: start recording if pctl.record_stream (rec button is current disabled for GST in UI) # encode to OGG and output file to prefs.encoder_output # automatically name files and split on metadata change # unload: Stop, cleanup and exit thread # done: Tell the main thread we finished doing a special request it was waiting for (such as unload) # Todo: Visualisers (Hard) # Ideally we would want the same visual effect as the old BASS based visualisers. # What we want to do is constantly get binned spectrum data and pass it to the UI. # Specifically, format used with BASS module is: # - An FFT of sample data with Hanning window applied # - 1024 samples (returns first half, 512 values) # - Non-complex (magnitudes) # - Combined left and right channels # - Binned to particular numbers of bars and passed onto UI after some scaling and truncating pctl.download_time = 0 url = None if command == 'open' and pctl.target_object: # print("Start track") track = pctl.target_object if (tauon.spot_ctl.playing or tauon.spot_ctl.coasting ) and not track.file_ext == "SPTY": tauon.spot_ctl.control("stop") if tauon.stream_proxy.download_running: tauon.stream_proxy.stop() # Check if the file exists, mark it as missing if not if track.is_network: if track.file_ext == "SPTY": tauon.level_train.clear() if self.play_state > 0: self.playbin.set_state(Gst.State.READY) self.play_state = 0 try: tauon.spot_ctl.play_target(track.url_key) except: print("Failed to start Spotify track") pctl.playerCommand = "stop" pctl.playerCommandReady = True GLib.timeout_add(19, self.main_callback) return try: url, params = pctl.get_url(track) self.urlparams = url, params except: time.sleep(0.1) gui.show_message("Connection error", "Bad login? Server offline?", mode='info') pctl.stop() pctl.playerCommand = "" self.main_callback() return # If the target is a file, check that is exists elif os.path.isfile(track.fullpath): if not track.found: pctl.reset_missing_flags() else: # File does not exist, force trigger an advance track.found = False tauon.console.print("Missing File: " + track.fullpath, 2) pctl.playing_state = 0 pctl.jump_time = 0 # print("FORCE JUMP") pctl.advance(inplace=True, play=True) GLib.timeout_add(19, self.main_callback) return gapless = False current_time = 0 current_duration = 0 if track.is_network: if params: self.url = url + ".view?" + urllib.parse.urlencode( params) else: self.url = url if self.play_state != 0: # Determine time position of currently playing track current_time = self.playbin.query_position( Gst.Format.TIME)[1] / Gst.SECOND current_duration = self.playbin.query_duration( Gst.Format.TIME)[1] / Gst.SECOND # print("We are " + str(current_duration - current_time) + " seconds from end.") if self.play_state != 1: self.loaded_track = None # If we are close to the end of the track, try transition gaplessly if self.play_state == 1 and pctl.start_time_target == 0 and pctl.jump_time == 0 and \ current_duration - current_time < 5.5 and not pctl.playerSubCommand == 'now' \ and self.end_timer.get() > 3: gapless = True # if self.play_state == 1 and self.loaded_track and self.loaded_track.is_network: # # Gst may report wrong length for network tracks, use known length instead # if pctl.playing_time < self.loaded_track.length - 4: # gapless = False # We're not at the end of the last track so reset the pipeline if not gapless: self.playbin.set_state(Gst.State.NULL) tauon.level_train.clear() pctl.playerSubCommand = "" self.play_state = 1 self.save_temp = tauon.temp_audio + "/" + str( track.index) + "-audio" # shoot_dl = threading.Thread(target=self.download_part, # args=([url, self.save_temp, params, track.url_key])) # shoot_dl.daemon = True # shoot_dl.start() self.using_cache = False self.id = track.url_key if url: # self.playbin.set_property('uri', # 'file://' + urllib.parse.quote(os.path.abspath(self.save_temp))) if self.dl_ready and os.path.exists(self.save_temp): self.using_cache = True self.playbin.set_property( 'uri', 'file://' + urllib.parse.quote( os.path.abspath(self.save_temp))) else: self.playbin.set_property('uri', self.url) else: # Play file on disk self.playbin.set_property( 'uri', 'file://' + urllib.parse.quote( os.path.abspath(track.fullpath))) if pctl.start_time_target > 0: self.playbin.set_property('volume', 0.0) else: self.playbin.set_property('volume', pctl.player_volume / 100) self.playbin.set_state(Gst.State.PLAYING) if pctl.jump_time == 0 and not pctl.playerCommand == "seek": pctl.playing_time = 0 # The position to start is not always the beginning of the file, so seek to position if pctl.start_time_target > 0 or pctl.jump_time > 0: tries = 0 while tries < 150: time.sleep(0.03) r = self.playbin.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, (pctl.start_time_target + pctl.jump_time) * Gst.SECOND) if r: break tries += 1 if tries > 2: print("Seek failed, retrying...") print(tries) pctl.playing_time = 0 gui.update = 1 self.playbin.set_property('volume', pctl.player_volume / 100) if gapless: # Hold thread while a gapless transition is in progress t = 0 # print("Gapless go") while self.playbin.query_position(Gst.Format.TIME)[ 1] / Gst.SECOND >= current_time > 0: time.sleep(0.02) t += 1 if self.playbin.get_state( 0).state != Gst.State.PLAYING: break if t > 250: print("Gonna stop waiting..." ) # Cant wait forever break if pctl.playerCommand == 'open' and pctl.playerCommandReady: # Cancel the gapless transition print("Cancel transition") self.playbin.set_state(Gst.State.NULL) time.sleep(0.5) GLib.timeout_add(19, self.main_callback) return add_time = max(min(self.player_timer.hit(), 3), 0) if self.loaded_track and self.loaded_track in pctl.master_library.values( ): star_store.add(self.loaded_track.index, add_time) self.loaded_track = track pctl.jump_time = 0 #time.sleep(1) add_time = self.player_timer.hit() if add_time > 2: add_time = 2 if add_time < 0: add_time = 0 pctl.playing_time += add_time t = self.playbin.query_position(Gst.Format.TIME) if t[0]: pctl.decode_time = ( t[1] / Gst.SECOND) - self.loaded_track.start_time else: pctl.decode_time = pctl.playing_time if self.loaded_track: star_store.add(self.loaded_track.index, add_time) self.check_duration() self.player_timer.hit() time.sleep(0.5) elif command == 'url': # Stop if playing or paused if self.play_state == 1 or self.play_state == 2 or self.play_state == 3: self.playbin.set_state(Gst.State.NULL) time.sleep(0.1) w = 0 while len(tauon.stream_proxy.chunks) < 50: time.sleep(0.01) w += 1 if w > 500: print("Taking too long!") tauon.stream_proxy.stop() pctl.playerCommand = 'stop' pctl.playerCommandReady = True break else: # Open URL stream self.playbin.set_property('uri', pctl.url) self.playbin.set_property('volume', pctl.player_volume / 100) self.buffering = False self.playbin.set_state(Gst.State.PLAYING) self.play_state = 3 self.player_timer.hit() elif command == 'seteq': self.set_eq() # for i, level in enumerate(prefs.eq): # if prefs.use_eq: # self._eq.set_property("band" + str(i), level * -1) # else: # self._eq.set_property("band" + str(i), 0.0) elif command == 'volume': if tauon.spot_ctl.coasting or tauon.spot_ctl.playing: tauon.spot_ctl.control("volume", int(pctl.player_volume)) elif self.play_state == 1 or self.play_state == 3: success, current_time = self.playbin.query_position( Gst.Format.TIME) self.playbin.set_state(Gst.State.PLAYING) if success and False: start = current_time + ((150 / 1000) * Gst.SECOND) end = current_time + ((200 / 1000) * Gst.SECOND) self.c_source.set( start, self._vol.get_property('volume') / 10) self.c_source.set(end, (pctl.player_volume / 100) / 10) time.sleep(0.6) self.c_source.unset_all() else: self.playbin.set_property('volume', pctl.player_volume / 100) elif command == 'runstop': if self.play_state != 0: # Determine time position of currently playing track current_time = self.playbin.query_position( Gst.Format.TIME)[1] / Gst.SECOND current_duration = self.playbin.query_duration( Gst.Format.TIME)[1] / Gst.SECOND if current_duration - current_time < 5.5: time.sleep((current_duration - current_time) + 1) self.playbin.set_state(Gst.State.READY) else: self.playbin.set_state(Gst.State.READY) else: self.playbin.set_state(Gst.State.READY) self.play_state = 0 pctl.playerSubCommand = "stopped" elif command == 'stop': if self.play_state > 0: if prefs.use_pause_fade: success, current_time = self.playbin.query_position( Gst.Format.TIME) if success: start = current_time + (150 / 1000 * Gst.SECOND) end = current_time + (prefs.pause_fade_time / 1000 * Gst.SECOND) self.c_source.set( start, (pctl.player_volume / 100) / 10) self.c_source.set(end, 0.0) time.sleep(prefs.pause_fade_time / 1000) time.sleep(0.05) self.c_source.unset_all() self.playbin.set_state(Gst.State.NULL) time.sleep(0.1) self._vol.set_property("volume", pctl.player_volume / 100) self.play_state = 0 pctl.playerSubCommand = "stopped" elif command == 'seek': self.seek_timer.set() if tauon.spot_ctl.coasting or tauon.spot_ctl.playing: tauon.spot_ctl.control("seek", int(pctl.new_time * 1000)) pctl.playing_time = pctl.new_time elif self.play_state > 0: if not self.using_cache and pctl.target_object.is_network and \ not pctl.target_object.file_ext == "KOEL" and \ not pctl.target_object.file_ext == "SUB" and \ not pctl.target_object.file_ext == "JELY": if not os.path.exists(tauon.temp_audio): os.makedirs(tauon.temp_audio) listing = os.listdir(tauon.temp_audio) full = [ os.path.join(tauon.temp_audio, x) for x in listing ] size = get_folder_size(tauon.temp_audio) / 1000000 print(f"Audio cache size is {size}MB") if size > 120: oldest_file = min(full, key=os.path.getctime) print("Cache full, delete oldest cached file") os.remove(oldest_file) pctl.playing_time = 0 self.playbin.set_state(Gst.State.NULL) self.dl_ready = False url, params = self.urlparams shoot_dl = threading.Thread( target=self.download_part, args=([ url, self.save_temp, params, pctl.target_object.url_key ])) shoot_dl.daemon = True shoot_dl.start() pctl.playerCommand = "" while not self.dl_ready: # print("waiting...") time.sleep(0.25) if pctl.playerCommandReady and pctl.playerCommand != "seek": print("BREAK!") self.main_callback() return self.playbin.set_property( 'uri', 'file://' + urllib.parse.quote( os.path.abspath(self.save_temp))) #time.sleep(0.05) self.playbin.set_state(Gst.State.PLAYING) self.using_cache = True time.sleep(0.1) self.playbin.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, (pctl.new_time + pctl.start_time_target) * Gst.SECOND) # It may take a moment for seeking to update when streaming, so for better UI feedback we'll # update the seek indicator immediately and hold the thread for a moment if pctl.target_object.is_network: pctl.playing_time = pctl.new_time + pctl.start_time_target pctl.decode_time = pctl.playing_time time.sleep(0.25) elif command == 'pauseon': self.player_timer.hit() self.play_state = 2 if prefs.use_pause_fade: success, current_time = self.playbin.query_position( Gst.Format.TIME) if success: start = current_time + (150 / 1000 * Gst.SECOND) end = current_time + (prefs.pause_fade_time / 1000 * Gst.SECOND) self.c_source.set(start, (pctl.player_volume / 100) / 10) self.c_source.set(end, 0.0) time.sleep(prefs.pause_fade_time / 1000) self.c_source.unset_all() self.playbin.set_state(Gst.State.PAUSED) elif command == 'pauseoff': self.player_timer.hit() if not prefs.use_pause_fade: self.playbin.set_state(Gst.State.PLAYING) else: self._vol.set_property("volume", 0.0) success, current_time = self.playbin.query_position( Gst.Format.TIME) self.playbin.set_state(Gst.State.PLAYING) if success: start = current_time + (150 / 1000 * Gst.SECOND) end = current_time + ( (prefs.pause_fade_time / 1000) * Gst.SECOND) self.c_source.set(start, 0.0) self.c_source.set(end, (pctl.player_volume / 100) / 10) time.sleep(prefs.pause_fade_time / 1000) time.sleep(0.05) self.c_source.unset_all() self._vol.set_property("volume", pctl.player_volume / 100) self.play_state = 1 elif command == 'unload': if self.play_state > 0: self.playbin.set_state(Gst.State.NULL) time.sleep(0.05) print("Unload GStreamer") self.mainloop.quit() pctl.playerCommand = 'done' return if self.play_state == 3: if self.playbin.get_state(0).state == Gst.State.PLAYING: add_time = self.player_timer.hit() if add_time > 2: add_time = 2 if add_time < 0: add_time = 0 pctl.playing_time += add_time pctl.decode_time = pctl.playing_time if self.play_state == 1: # Get jump in time since last call add_time = self.player_timer.hit() # Limit the jump. if add_time > 2: add_time = 2 if add_time < 0: add_time = 0 # # Progress main seek head if self.playbin.get_state( 0).state == Gst.State.PLAYING and self.seek_timer.get( ) > 1 and not pctl.playerCommandReady: pctl.playing_time += add_time p = max(0, (self.playbin.query_position(Gst.Format.TIME)[1] / Gst.SECOND) - pctl.start_time_target) if abs(pctl.playing_time - p) > 0.2: pctl.playing_time = p pctl.decode_time = p else: # We're supposed to be playing but it's not? Give it a push I guess. #self.playbin.set_state(Gst.State.PLAYING) pctl.playing_time += add_time pctl.decode_time = pctl.playing_time # Other things we need to progress such as scrobbling if pctl.playing_time < 3 and pctl.a_time < 3: pctl.a_time = pctl.playing_time else: pctl.a_time += add_time pctl.total_playtime += add_time lfm_scrobbler.update( add_time ) # This handles other scrobblers such as listenbrainz also # Update track total playtime if len(pctl.track_queue) > 0 and 2 > add_time > 0: star_store.add(pctl.track_queue[pctl.queue_step], add_time) if not pctl.running: # print("unloading gstreamer") if self.play_state > 0: self.playbin.set_state(Gst.State.NULL) time.sleep(0.5) self.mainloop.quit() pctl.playerCommand = 'done' else: if gui.vis == 1: GLib.timeout_add(19, self.main_callback) else: GLib.timeout_add(50, self.main_callback) def exit(self): print("GStreamer unloaded") pctl.playerCommand = 'done'
class Jellyfin(): def __init__(self, tauon): self.tauon = tauon self.pctl = tauon.pctl self.prefs = tauon.prefs self.gui = tauon.gui self.scanning = False self.connected = False self.accessToken = None self.userId = None self.currentId = None self.session_thread_active = False self.session_status = 0 self.session_item_id = None self.session_update_timer = Timer() self.session_last_item = None def _get_jellyfin_auth(self): auth_str = f"MediaBrowser Client={self.tauon.t_title}, Device={self.tauon.device}, DeviceId=-, Version={self.tauon.t_version}" if self.accessToken: auth_str += f", Token={self.accessToken}" return auth_str def _authenticate(self, debug=False): username = self.prefs.jelly_username password = self.prefs.jelly_password server = self.prefs.jelly_server_url try: response = requests.post( f"{server}/Users/AuthenticateByName", headers={ "Content-type": "application/json", "X-Application": self.tauon.t_agent, "x-emby-authorization": self._get_jellyfin_auth() }, data=json.dumps({ "username": username, "Pw": password }), ) except: self.gui.show_message( "Could not establish connection to server.", "Check server is running and URL is correct.", mode="error") return if response.status_code == 200: info = response.json() self.accessToken = info["AccessToken"] self.userId = info["User"]["Id"] self.connected = True if debug: self.gui.show_message( "Connection and authorisation successful", mode="done") else: if response.status_code == 401: self.gui.show_message("401 Authentication failed", "Check username and password.", mode="warning") else: self.gui.show_message( "Jellyfin auth error", f"{response.status_code} {response.text}", mode="warning") def test(self): self._authenticate(debug=True) def resolve_stream(self, id): if not self.connected or not self.accessToken: self._authenticate() if not self.connected: return "" base_url = f"{self.prefs.jelly_server_url}/Audio/{id}/stream" headers = { "Token": self.accessToken, "X-Application": "Tauon/1.0", "x-emby-authorization": self._get_jellyfin_auth() } params = {"UserId": self.userId, "static": "true"} if self.prefs.network_stream_bitrate > 0: params["MaxStreamingBitrate"] = self.prefs.network_stream_bitrate url = base_url + "?" + requests.compat.urlencode(params) if not self.session_thread_active: shoot = threading.Thread(target=self.session) shoot.daemon = True shoot.start() return base_url, params def get_cover(self, track): if not self.connected or not self.accessToken: self._authenticate() if not self.connected: return None if not track.art_url_key: return None headers = { "Token": self.accessToken, "X-Application": "Tauon/1.0", "x-emby-authorization": self._get_jellyfin_auth() } params = {} base_url = f"{self.prefs.jelly_server_url}/Items/{track.art_url_key}/Images/Primary" response = requests.get(base_url, headers=headers, params=params) if response.status_code == 200: return io.BytesIO(response.content) else: print("Jellyfin album art api error:", response.status_code, response.text) return None def ingest_library(self, return_list=False): self.gui.update += 1 self.scanning = True self.gui.to_got = 0 print("Prepare for Jellyfin library import...") if not self.connected or not self.accessToken: self._authenticate() if not self.connected: self.scanning = False if not return_list: self.tauon.gui.show_message("Error connecting to Jellyfin") return [] playlist = [] # This code is to identify if a track has already been imported existing = {} for track_id, track in self.pctl.master_library.items(): if track.is_network and track.file_ext == "JELY": existing[track.url_key] = track_id print("Get items...") try: response = requests.get( f"{self.prefs.jelly_server_url}/Users/{self.userId}/Items", headers={ "Token": self.accessToken, "X-Application": "Tauon/1.0", "x-emby-authorization": self._get_jellyfin_auth() }, params={ "recursive": True, "filter": "music" }) except: self.gui.show_message("Error connecting to Jellyfin for Import", mode="error") self.scanning = False return if response.status_code == 200: print("Connection successful, soring items...") # filter audio items only audio_items = list( filter(lambda item: item["Type"] == "Audio", response.json()["Items"])) # sort by artist, then album, then track number sorted_items = sorted( audio_items, key=lambda item: (item.get("AlbumArtist", ""), item.get("Album", ""), item.get("IndexNumber", -1))) # group by parent grouped_items = itertools.groupby( sorted_items, lambda item: (item.get("AlbumArtist", "") + " - " + item.get( "Album", "")).strip("- ")) else: self.scanning = False self.tauon.gui.show_message("Error accessing Jellyfin", mode="warning") return for parent, items in grouped_items: for track in items: id = self.pctl.master_count # id here is tauons track_id for the track existing_track = existing.get(track.get("Id")) replace_existing = existing_track is not None if replace_existing: id = existing_track nt = self.tauon.TrackClass() nt.index = id # this is tauons track id nt.track_number = str(track.get("IndexNumber", "")) nt.disc_number = str(track.get("ParentIndexNumber", "")) nt.file_ext = "JELY" nt.parent_folder_path = parent nt.parent_folder_name = parent nt.album_artist = track.get("AlbumArtist", "") nt.artist = track.get("AlbumArtist", "") nt.title = track.get("Name", "") nt.album = track.get("Album", "") nt.length = track.get("RunTimeTicks", 0) / 10000000 # needs to be in seconds nt.date = str(track.get("ProductionYear")) nt.is_network = True nt.url_key = track.get("Id") nt.art_url_key = track.get("AlbumId") if track.get( "AlbumPrimaryImageTag", False) else None self.pctl.master_library[id] = nt if not replace_existing: self.pctl.master_count += 1 playlist.append(nt.index) self.scanning = False print("Jellyfin import complete") if return_list: return playlist self.pctl.multi_playlist.append( self.tauon.pl_gen(title="Jellyfin Collection", playlist=playlist)) self.pctl.gen_codes[self.tauon.pl_to_id( len(self.pctl.multi_playlist) - 1)] = "jelly" self.tauon.switch_playlist(len(self.pctl.multi_playlist) - 1) def session_item(self, track): return { "QueueableMediaTypes": ["Audio"], "CanSeek": True, "ItemId": track.url_key, "IsPaused": self.pctl.playing_state == 2, "IsMuted": self.pctl.player_volume == 0, "PositionTicks": int(self.pctl.playing_time * 10000000), "PlayMethod": "DirectStream", "PlaySessionId": "0", } def session(self): if not self.connected: return self.session_thread_active = True while True: time.sleep(1) track = self.pctl.playing_object() if track.file_ext != "JELY" or (self.session_status == 0 and self.pctl.playing_state == 0): if self.session_status != 0: data = self.session_last_item self.session_send("Sessions/Playing/Stopped", data) self.session_status = 0 self.session_thread_active = False return if (self.session_status == 0 or self.session_item_id != track.index ) and self.pctl.playing_state == 1: data = self.session_item(track) self.session_send("Sessions/Playing", data) self.session_update_timer.set() self.session_status = 1 self.session_item_id = track.index self.session_last_item = data elif self.session_status == 1 and self.session_update_timer.get( ) >= 10: data = self.session_item(track) data["EventName"] = "TimeUpdate" self.session_send("Sessions/Playing/Progress", data) self.session_update_timer.set() elif self.session_status in (1, 2) and self.pctl.playing_state in (0, 3): data = self.session_last_item self.session_send("Sessions/Playing/Stopped", data) self.session_status = 0 elif self.session_status == 1 and self.pctl.playing_state == 2: data = self.session_item(track) data["EventName"] = "Pause" self.session_send("Sessions/Playing/Progress", data) self.session_update_timer.set() self.session_status = 2 elif self.session_status == 2 and self.pctl.playing_state == 1: data = self.session_item(track) data["EventName"] = "Unpause" self.session_send("Sessions/Playing/Progress", data) self.session_update_timer.set() self.session_status = 1 def session_send(self, point, data): response = requests.post(f"{self.prefs.jelly_server_url}/{point}", data=json.dumps(data), headers={ "Token": self.accessToken, "X-Application": "Tauon/1.0", "x-emby-authorization": self._get_jellyfin_auth(), "Content-Type": "application/json" })
class SpotCtl: def __init__(self, tauon): self.tauon = tauon self.strings = tauon.strings self.start_timer = Timer() self.status = 0 self.spotify = None self.loaded_art = "" self.playing = False self.coasting = False self.paused = False self.token = None self.cred = None self.started_once = False self.redirect_uri = f"http://localhost:7811/spotredir" self.current_imports = {} self.spotify_com = False self.progress_timer = Timer() self.update_timer = Timer() self.token_path = os.path.join(self.tauon.user_directory, "spot-r-token") def prep_cred(self): try: rc = tk.RefreshingCredentials except: rc = tk.auth.RefreshingCredentials self.cred = rc(client_id=self.tauon.prefs.spot_client, client_secret=self.tauon.prefs.spot_secret, redirect_uri=self.redirect_uri) def connect(self): if self.cred is None: self.prep_cred() if self.spotify is None: if self.token is None: self.load_token() if self.token: print("Init spotify support") self.spotify = tk.Spotify(self.token) def paste_code(self, code): if self.cred is None: self.prep_cred() self.token = self.cred.request_user_token(code) if self.token: self.save_token() self.tauon.gui.show_message(self.strings.spotify_account_connected, mode="done") def save_token(self): if self.token: pickle.dump(self.token, open(self.token_path, "wb")) def delete_token(self): if os.path.isfile(self.token_path): os.remove(self.token_path) self.token = None def load_token(self): if os.path.isfile(self.token_path): try: f = open(self.token_path, "rb") self.token = pickle.load(f) f.close() print("Loaded spotify token from file") except: print("ERROR LOADING TOKEN. DELETING TOKEN ON DISK.") self.tauon.gui.show_message("Upgrade issue. Please re-authroise Spotify in settings!", mode="warning") self.delete_token() def auth(self): if not tekore_imported: self.tauon.gui.show_message("python-tekore not installed", "If you installed via AUR, you'll need to install this optional dependency, then restart Tauon.", mode="error") return if len(self.tauon.prefs.spot_client) != 32 or len(self.tauon.prefs.spot_secret) != 32: self.tauon.gui.show_message("Invalid client ID or secret", mode="error") return if self.cred is None: self.prep_cred() url = self.cred.user_authorisation_url(scope="user-read-playback-position streaming user-modify-playback-state user-library-modify user-library-read user-read-currently-playing user-read-playback-state") webbrowser.open(url, new=2, autoraise=True) def control(self, command, param=None): try: if command == "pause" and (self.playing or self.coasting) and not self.paused: self.spotify.playback_pause() self.paused = True self.start_timer.set() if command == "stop" and (self.playing or self.coasting): self.paused = False self.playing = False self.coasting = False self.spotify.playback_pause() self.start_timer.set() if command == "resume" and (self.coasting or self.playing) and self.paused: self.spotify.playback_resume() self.paused = False self.start_timer.set() if command == "volume": self.spotify.playback_volume(param) if command == "seek": self.spotify.playback_seek(param) self.start_timer.set() if command == "next": self.spotify.playback_next(param) #self.start_timer.set() if command == "previous": self.spotify.playback_previous(param) #self.start_timer.set() except Exception as e: print(repr(e)) if "No active device found" in repr(e): self.tauon.gui.show_message("It looks like there are no more active Spotify devices") def get_album_url_from_local(self, track_object): if "spotify-album-url" in track_object.misc: return track_object.misc["spotify-album-url"] self.connect() if not self.spotify: return None results = self.spotify.search(track_object.artist + " " + track_object.album, types=('album',), limit=1) for album in results[0].items: return album.external_urls["spotify"] return None def get_playlists(self): self.connect() if not self.spotify: return None results = self.spotify.playlists(self.spotify.current_user().id) print(results) def search(self, text): self.connect() if not self.spotify: return results = self.spotify.search(text, types=('artist', 'album',), limit=20 ) finds = [] self.tauon.QuickThumbnail.queue.clear() if results[0]: for album in results[0].items[0:1]: img = self.tauon.QuickThumbnail() img.url = album.images[-1].url img.size = round(50 * self.tauon.gui.scale) self.tauon.QuickThumbnail().items.append(img) self.tauon.QuickThumbnail().queue.append(img) try: self.tauon.gall_ren.lock.release() except: pass finds.append((11, (album.name, album.artists[0].name), album.external_urls["spotify"], 0, 0, img)) for artist in results[1].items[0:1]: finds.insert(2, (10, artist.name, artist.external_urls["spotify"], 0, 0, None)) for i, album in enumerate(results[0].items[1:]): img = self.tauon.QuickThumbnail() img.url = album.images[-1].url img.size = round(50 * self.tauon.gui.scale) self.tauon.QuickThumbnail().items.append(img) if i < 10: self.tauon.QuickThumbnail().queue.append(img) try: self.tauon.gall_ren.lock.release() except: pass finds.append((11, (album.name, album.artists[0].name), album.external_urls["spotify"], 0, 0, img)) # for artist in results[1].items[1:2]: # finds.append((10, artist.name, artist.external_urls["spotify"], 0, 0, None)) # for album in results[0].items[8:]: # finds.append((11, (album.name, album.artists[0].name), album.external_urls["spotify"], 0, 0, None)) return finds def search_track(self, track): if track is None: return self.connect() if not self.spotify: return if track.artist and track.title: results = self.spotify.search(track.artist + " " + track.title, types=('track',), limit=1 ) print(dir(results)) print(results) def prime_device(self): self.connect() if not self.spotify: return devices = self.spotify.playback_devices() if not devices: # webbrowser.open("https://open.spotify.com/", new=2, autoraise=False) # tries = 0 # while not devices: # time.sleep(2) # if tries == 0: # self.tauon.focus_window() # devices = self.spotify.playback_devices() # tries += 1 # if tries > 4: # break # if not devices: # return False return False for d in devices: if d.is_active: return None for d in devices: if not d.is_restricted: return d.id return None def play_target(self, id): self.coasting = False self.connect() if not self.spotify: return d_id = self.prime_device() # if d_id is False: # return #if self.tauon.pctl.playing_state == 1 and self.playing and self.tauon.pctl.playing_time #try: if d_id is False: if self.tauon.prefs.launch_spotify_web: webbrowser.open("https://open.spotify.com/", new=2, autoraise=False) tries = 0 while True: time.sleep(2) if tries == 0: self.tauon.focus_window() devices = self.spotify.playback_devices() if devices: self.spotify.playback_start_tracks([id], device_id=devices[0].id) break tries += 1 if tries > 6: self.tauon.pctl.stop() self.tauon.gui.show_message(self.strings.spotify_error_starting, mode="error") return else: subprocess.run(["xdg-open", "spotify:track"]) print("LAUNCH SPOTIFY") time.sleep(3) tries = 0 playing = False while True: print("WAIT FOR DEVICE...") devices = self.spotify.playback_devices() if devices: print("DEVICE FOUND") self.tauon.focus_window() time.sleep(1) print("ATTEMPT START") self.spotify.playback_start_tracks([id], device_id=devices[0].id) while True: result = self.spotify.playback_currently_playing() if result and result.is_playing: playing = True print("TRACK START SUCCESS") break time.sleep(2) tries += 1 print("NOT PLAYING YET...") if tries > 6: break if playing: break tries += 1 if tries > 6: print("TOO MANY TRIES") self.tauon.pctl.stop() self.tauon.gui.show_message(self.strings.spotify_error_starting, mode="error") return time.sleep(2) else: self.spotify.playback_start_tracks([id], device_id=d_id) # except Exception as e: # self.tauon.gui.show_message("Error. Do you have playback started somewhere?", mode="error") self.playing = True self.started_once = True self.progress_timer.set() self.start_timer.set() self.tauon.gui.pl_update += 1 def get_library_albums(self): self.connect() if not self.spotify: return albums = self.spotify.saved_albums() playlist = [] self.update_existing_import_list() pages = self.spotify.all_pages(albums) for page in pages: for a in page.items: self.load_album(a.album, playlist) self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title=self.strings.spotify_albums, playlist=playlist)) self.spotify_com = False def append_album(self, url, playlist_number=None, return_list=False): self.connect() if not self.spotify: return id = url.strip("/").split("/")[-1] album = self.spotify.album(id) playlist = [] self.update_existing_import_list() self.load_album(album, playlist) if return_list: return playlist if playlist_number is None: playlist_number = self.tauon.pctl.active_playlist_viewing self.tauon.pctl.multi_playlist[playlist_number][2].extend(playlist) self.tauon.gui.pl_update += 1 def playlist(self, url): self.connect() if not self.spotify: return id = url.strip("/").split("/")[-1] p = self.spotify.playlist(id) playlist = [] self.update_existing_import_list() for item in p.tracks.items: nt = self.load_track(item.track) self.tauon.pctl.master_library[nt.index] = nt playlist.append(nt.index) title = p.name + " by " + p.owner.display_name self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title=title, playlist=playlist)) self.tauon.switch_playlist(len(self.tauon.pctl.multi_playlist) - 1) def artist_playlist(self, url): id = url.strip("/").split("/")[-1] artist = self.spotify.artist(id) artist_albums = self.spotify.artist_albums(id, limit=30, include_groups=["album"]) playlist = [] self.update_existing_import_list() for a in artist_albums.items: full_album = self.spotify.album(a.id) self.load_album(full_album, playlist) self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title="Spotify: " + artist.name, playlist=playlist)) self.tauon.switch_playlist(len(self.tauon.pctl.multi_playlist) - 1) def update_existing_import_list(self): self.current_imports.clear() for tr in self.tauon.pctl.master_library.values(): if "spotify-track-url" in tr.misc: self.current_imports[tr.misc["spotify-track-url"]] = tr def load_album(self, album, playlist): #a = item album_url = album.external_urls["spotify"] art_url = album.images[0].url album_name = album.name total_tracks = album.total_tracks date = album.release_date album_artist = album.artists[0].name id = album.id parent = (album_artist + " - " + album_name).strip("- ") # print(a.release_date, a.name) for track in album.tracks.items: pr = self.current_imports.get(track.external_urls["spotify"]) if pr: new = False nt = pr else: new = True nt = self.tauon.TrackClass() nt.index = self.tauon.pctl.master_count nt.is_network = True nt.file_ext = "SPTY" nt.url_key = track.id nt.misc["spotify-artist-url"] = track.artists[0].external_urls["spotify"] nt.misc["spotify-album-url"] = album_url nt.misc["spotify-track-url"] = track.external_urls["spotify"] nt.artist = track.artists[0].name nt.album_artist = album_artist nt.date = date nt.album = album_name nt.disc_number = track.disc_number #nt.disc_total = nt.length = track.duration_ms / 1000 nt.title = track.name nt.track_number = track.track_number nt.track_total = total_tracks nt.art_url_key = art_url nt.parent_folder_path = parent nt.parent_folder_name = parent if new: self.tauon.pctl.master_count += 1 self.tauon.pctl.master_library[nt.index] = nt playlist.append(nt.index) def load_track(self, track, update_master_count=True): pr = self.current_imports.get(track.external_urls["spotify"]) if pr: new = False nt = pr else: new = True nt = self.tauon.TrackClass() nt.index = self.tauon.pctl.master_count nt.is_network = True nt.file_ext = "SPTY" nt.url_key = track.id if new: nt.misc["spotify-artist-url"] = track.artists[0].external_urls["spotify"] # nt.misc["spotify-album-url"] = album_url nt.misc["spotify-track-url"] = track.external_urls["spotify"] nt.artist = track.artists[0].name nt.album_artist = track.album.artists[0].name nt.date = track.album.release_date nt.album = track.album.name nt.disc_number = track.disc_number nt.length = track.duration_ms / 1000 nt.title = track.name nt.track_number = track.track_number # nt.track_total = total_tracks nt.art_url_key = track.album.images[0].url parent = (nt.album_artist + " - " + nt.album).strip("- ") nt.parent_folder_path = parent nt.parent_folder_name = parent if update_master_count and new: self.tauon.pctl.master_count += 1 return nt def like_track(self, tract_object): track_url = tract_object.misc.get("spotify-track-url", False) if track_url: id = track_url.strip("/").split("/")[-1] results = self.spotify.saved_tracks_contains([id]) if not results or results[0] is False: self.spotify.saved_tracks_add([id]) tract_object.misc["spotify-liked"] = True self.tauon.gui.show_message(self.strings.spotify_like_added, mode="done") return self.tauon.gui.show_message(self.strings.spotify_already_liked) return def unlike_track(self, tract_object): track_url = tract_object.misc.get("spotify-track-url", False) if track_url: id = track_url.strip("/").split("/")[-1] results = self.spotify.saved_tracks_contains([id]) if not results or results[0] is True: self.spotify.saved_tracks_delete([id]) tract_object.pop("spotify-liked", None) self.tauon.gui.show_message(self.strings.spotify_un_liked, mode="done") return self.tauon.gui.show_message(self.strings.spotify_already_un_liked) return def get_library_likes(self): self.connect() if not self.spotify: return self.update_existing_import_list() tracks = self.spotify.saved_tracks() playlist = [] for tr in self.tauon.pctl.master_library.values(): tr.misc.pop("spotify-liked", None) pages = self.spotify.all_pages(tracks) for page in pages: for item in page.items: nt = self.load_track(item.track) self.tauon.pctl.master_library[nt.index] = nt playlist.append(nt.index) nt.misc["spotify-liked"] = True for p in self.tauon.pctl.multi_playlist: if p[0] == self.tauon.strings.spotify_likes: p[2][:] = playlist[:] return self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title=self.tauon.strings.spotify_likes, playlist=playlist)) self.spotify_com = False def monitor(self): tr = self.tauon.pctl.playing_object() if self.playing and self.start_timer.get() > 6 and self.tauon.pctl.playing_time + 5 < tr.length: result = self.spotify.playback_currently_playing() if (result is None or result.item is None or not result.is_playing) or tr is None: self.playing = False self.tauon.pctl.stop() return if result.item.name != tr.title: self.tauon.pctl.playing_state = 3 self.playing = False self.coasting = True self.coast_update(result) self.tauon.gui.pl_update += 2 return p = result.progress_ms if p is not None: self.tauon.pctl.playing_time = p / 1000 self.tauon.pctl.decode_time = self.tauon.pctl.playing_time def update(self, start=False): if self.playing: self.coasting = False return self.connect() if not self.spotify: return result = self.spotify.playback_currently_playing() if self.playing or (not self.coasting and not start): return if result is None or result.is_playing is False: if self.coasting: if self.tauon.pctl.radio_image_bin: self.loaded_art = "" self.tauon.pctl.radio_image_bin.close() self.tauon.pctl.radio_image_bin = None self.tauon.dummy_track.artist = "" self.tauon.dummy_track.date = "" self.tauon.dummy_track.title = "" self.tauon.dummy_track.album = "" self.tauon.dummy_track.art_url_key = "" self.tauon.gui.clear_image_cache_next = True self.paused = True else: self.tauon.gui.show_message(self.strings.spotify_not_playing) return self.coasting = True self.started_once = True self.tauon.pctl.playing_state = 3 if result.is_playing: self.paused = False else: self.paused = True self.coast_update(result) def append_playing(self, playlist_number): if not self.coasting: return tr = self.tauon.pctl.playing_object() if tr and "spotify-album-url" in tr.misc: self.append_album(tr.misc["spotify-album-url"], playlist_number) def coast_update(self, result): self.tauon.dummy_track.artist = result.item.artists[0].name self.tauon.dummy_track.title = result.item.name self.tauon.dummy_track.album = result.item.album.name self.tauon.dummy_track.date = result.item.album.release_date self.tauon.dummy_track.file_ext = "Spotify" self.progress_timer.set() self.update_timer.set() d = result.item.duration_ms if d is not None: self.tauon.pctl.playing_length = d / 1000 p = result.progress_ms if p is not None: self.tauon.pctl.playing_time = p / 1000 self.tauon.pctl.decode_time = self.tauon.pctl.playing_time art_url = result.item.album.images[0].url self.tauon.dummy_track.url_key = result.item.id self.tauon.dummy_track.misc["spotify-album-url"] = result.item.album.external_urls["spotify"] self.tauon.dummy_track.misc["spotify-track-url"] = result.item.external_urls["spotify"] if art_url and self.loaded_art != art_url: self.loaded_art = art_url art_response = requests.get(art_url) if self.tauon.pctl.radio_image_bin: self.tauon.pctl.radio_image_bin.close() self.tauon.pctl.radio_image_bin = None self.tauon.pctl.radio_image_bin = io.BytesIO(art_response.content) self.tauon.pctl.radio_image_bin.seek(0) self.tauon.dummy_track.art_url_key = "ok" self.tauon.gui.clear_image_cache_next = True self.tauon.gui.update += 2 self.tauon.gui.pl_update += 1
class SpotCtl: def __init__(self, tauon): self.tauon = tauon self.strings = tauon.strings self.start_timer = Timer() self.status = 0 self.spotify = None self.loaded_art = "" self.playing = False self.coasting = False self.paused = False self.token = None self.cred = None self.started_once = False self.redirect_uri = f"http://localhost:7811/spotredir" self.current_imports = {} self.spotify_com = False self.sender = None self.cache_saved_albums = [] self.scope = "user-read-playback-position streaming user-modify-playback-state user-library-modify user-library-read user-read-currently-playing user-read-playback-state playlist-read-private playlist-modify-private playlist-modify-public" self.launching_spotify = False self.progress_timer = Timer() self.update_timer = Timer() self.token_path = os.path.join(self.tauon.user_directory, "spot-token-pkce") self.pkce_code = None def prep_cred(self): rc = tk.RefreshingCredentials self.cred = rc(client_id=self.tauon.prefs.spot_client, redirect_uri=self.redirect_uri) def connect(self): if not self.tauon.prefs.spotify_token or not self.tauon.prefs.spot_mode: return if len(self.tauon.prefs.spot_client) != 32: return if self.cred is None: self.prep_cred() if self.spotify is None: if self.token is None: self.load_token() if self.token: print("Init spotify support") self.sender = tk.RetryingSender(retries=3) self.spotify = tk.Spotify(self.token, sender=self.sender) def paste_code(self, code): if self.cred is None: self.prep_cred() self.token = self.cred.request_pkce_token(code.strip().strip("\n"), self.pkce_code) if self.token: self.save_token() self.tauon.gui.show_message(self.strings.spotify_account_connected, mode="done") def save_token(self): if self.token: f = open(self.token_path, "w") f.write(str(self.token.refresh_token)) f.close() self.tauon.prefs.spotify_token = str(self.token.refresh_token) def load_token(self): if os.path.isfile(self.token_path): f = open(self.token_path, "r") self.tauon.prefs.spotify_token = f.read().replace("\n", "").strip() f.close() if self.tauon.prefs.spotify_token: try: self.token = tk.refresh_pkce_token(self.tauon.prefs.spot_client, self.tauon.prefs.spotify_token) except: print("ERROR LOADING TOKEN") self.tauon.prefs.spotify_token = "" def delete_token(self): self.tauon.prefs.spotify_token = "" self.token = None if os.path.exists(self.token_path): os.remove(self.token_path) def auth(self): if not tekore_imported: self.tauon.gui.show_message("python-tekore not installed", "If you installed via AUR, you'll need to install this optional dependency, then restart Tauon.", mode="error") return if len(self.tauon.prefs.spot_client) != 32: self.tauon.gui.show_message("Invalid client ID. See Spotify tab in settings.", mode="error") return if self.cred is None: self.prep_cred() url, self.pkce_code = self.cred.pkce_user_authorisation(scope=self.scope) webbrowser.open(url, new=2, autoraise=True) def control(self, command, param=None): try: if command == "pause" and (self.playing or self.coasting) and not self.paused: self.spotify.playback_pause() self.paused = True self.start_timer.set() if command == "stop" and (self.playing or self.coasting): self.paused = False self.playing = False self.coasting = False self.spotify.playback_pause() self.start_timer.set() if command == "resume" and (self.coasting or self.playing) and self.paused: self.spotify.playback_resume() self.paused = False self.progress_timer.set() self.start_timer.set() if command == "volume": self.spotify.playback_volume(param) if command == "seek": self.spotify.playback_seek(param) self.start_timer.set() if command == "next": self.spotify.playback_next(param) #self.start_timer.set() if command == "previous": self.spotify.playback_previous(param) #self.start_timer.set() except Exception as e: print(repr(e)) if "No active device found" in repr(e): try: tr = self.tauon.pctl.playing_object() if command == "resume" and tr and tr.file_ext == "SPTY" and tr.url_key: self.tauon.gui.show_message("Resuming Spotify playback") p = self.tauon.pctl.playing_time self.play_target(tr.url_key) time.sleep(0.3) self.spotify.playback_seek(int(p * 1000)) self.tauon.gui.message_box = False self.tauon.gui.update += 1 return except: pass self.tauon.gui.show_message("It looks like there are no more active Spotify devices") def add_album_to_library(self, url): self.connect() if not self.spotify: return None id = url.strip("/").split("/")[-1] try: self.spotify.saved_albums_add([id]) if url not in self.cache_saved_albums: self.cache_saved_albums.append(url) except: print("Error saving album") def remove_album_from_library(self, url): self.connect() if not self.spotify: return None id = url.strip("/").split("/")[-1] try: self.spotify.saved_albums_delete([id]) if url in self.cache_saved_albums: self.cache_saved_albums.remove(url) except: print("Error removing album") def get_album_url_from_local(self, track_object): if "spotify-album-url" in track_object.misc: return track_object.misc["spotify-album-url"] self.connect() if not self.spotify: return None results = self.spotify.search(track_object.artist + " " + track_object.album, types=('album',), limit=1) for album in results[0].items: if "spotify" in album.external_urls: return album.external_urls["spotify"] return None def import_all_playlists(self): self.spotify_com = True playlists = self.get_playlist_list() if playlists: for item in playlists: self.playlist(item[1], silent=True) self.tauon.gui.update += 1 time.sleep(0.1) self.spotify_com = False if not playlists: self.tauon.gui.show_message(self.strings.spotify_need_enable) return self.tauon.gui.show_message(self.strings.spotify_import_complete, mode="done") def get_playlist_list(self): self.connect() if not self.spotify: self.tauon.gui.show_message(self.strings.spotify_need_enable) return None playlists = [] results = self.spotify.playlists(self.spotify.current_user().id) pages = self.spotify.all_pages(results) for page in pages: items = page.items for item in items: name = item.name url = item.external_urls["spotify"] playlists.append((name, url)) return playlists def search(self, text): self.connect() if not self.spotify: return results = self.spotify.search(text, types=('artist', 'album', 'track'), limit=20 ) finds = [] self.tauon.QuickThumbnail.queue.clear() if results[0]: for i, album in enumerate(results[0].items[1:]): img = self.tauon.QuickThumbnail() img.url = album.images[-1].url img.size = round(50 * self.tauon.gui.scale) self.tauon.QuickThumbnail().items.append(img) if i < 10: self.tauon.QuickThumbnail().queue.append(img) try: self.tauon.gall_ren.lock.release() except: pass finds.append((11, (album.name, album.artists[0].name), album.external_urls["spotify"], 0, 0, img)) for artist in results[1].items[0:1]: finds.insert(1, (10, artist.name, artist.external_urls["spotify"], 0, 0, None)) for artist in results[1].items[1:2]: finds.insert(11, (10, artist.name, artist.external_urls["spotify"], 0, 0, None)) for track in results[2].items[0:1]: finds.insert(2, (12, (track.name, track.artists[0].name), track.external_urls["spotify"], 0, 0, None)) for track in results[2].items[5:6]: finds.insert(8, (12, (track.name, track.artists[0].name), track.external_urls["spotify"], 0, 0, None)) return finds def search_track(self, track): if track is None: return self.connect() if not self.spotify: return if track.artist and track.title: results = self.spotify.search(track.artist + " " + track.title, types=('track',), limit=1 ) def prime_device(self): self.connect() if not self.spotify: return devices = self.spotify.playback_devices() if devices: pass else: print("No spotify devices found") if not devices: return False for d in devices: if d.is_active: return None for d in devices: if not d.is_restricted: return d.id return None def play_target(self, id): self.coasting = False self.connect() if not self.spotify: self.tauon.gui.show_message("Error. You may need to click Authorise in Settings > Accounts > Spotify.", mode="warning") return d_id = self.prime_device() # if d_id is False: # return #if self.tauon.pctl.playing_state == 1 and self.playing and self.tauon.pctl.playing_time #try: if d_id is False: self.launching_spotify = True self.tauon.gui.update += 1 if self.tauon.prefs.launch_spotify_web: webbrowser.open("https://open.spotify.com/", new=2, autoraise=False) tries = 0 while True: time.sleep(2) if tries == 0: self.tauon.focus_window() devices = self.spotify.playback_devices() if devices: self.progress_timer.set() self.spotify.playback_start_tracks([id], device_id=devices[0].id) break tries += 1 if tries > 6: self.tauon.pctl.stop() self.tauon.gui.show_message(self.strings.spotify_error_starting, mode="error") self.launching_spotify = False self.tauon.gui.update += 1 return else: subprocess.run(["xdg-open", "spotify:track"]) print("LAUNCH SPOTIFY") time.sleep(3) tries = 0 playing = False while True: print("WAIT FOR DEVICE...") devices = self.spotify.playback_devices() if devices: print("DEVICE FOUND") self.tauon.focus_window() time.sleep(1) print("ATTEMPT START") self.spotify.playback_start_tracks([id], device_id=devices[0].id) while True: result = self.spotify.playback_currently_playing() if result and result.is_playing: playing = True self.progress_timer.set() print("TRACK START SUCCESS") break time.sleep(2) tries += 1 print("NOT PLAYING YET...") if tries > 6: break if playing: break tries += 1 if tries > 6: print("TOO MANY TRIES") self.tauon.pctl.stop() self.tauon.gui.show_message(self.strings.spotify_error_starting, mode="error") self.launching_spotify = False self.tauon.gui.update += 1 return time.sleep(2) self.launching_spotify = False self.tauon.gui.update += 1 else: try: self.progress_timer.set() okay = False # Check conditions for a proper transition if self.playing: result = self.spotify.playback_currently_playing() if result and result.item and result.is_playing: remain = result.item.duration_ms - result.progress_ms if 1400 < remain < 3500: self.spotify.playback_queue_add("spotify:track:" + id, device_id=d_id) okay = True time.sleep(remain / 1000) self.progress_timer.set() time.sleep(1) result = self.spotify.playback_currently_playing() if not (result and result.item and result.is_playing): print("A queue transition failed") okay = False # Force a transition if not okay: self.spotify.playback_start_tracks([id], device_id=d_id) # except tk.client.decor.error.InternalServerError: # self.tauon.gui.show_message("Spotify server error. Maybe try again later.") # return except: self.tauon.gui.show_message("Spotify error, try again?", mode="warning") return # except Exception as e: # self.tauon.gui.show_message("Error. Do you have playback started somewhere?", mode="error") self.playing = True self.started_once = True self.start_timer.set() self.tauon.pctl.playing_time = 0 self.tauon.pctl.decode_time = 0 self.tauon.gui.pl_update += 1 def get_library_albums(self, return_list=False): self.connect() if not self.spotify: self.spotify_com = False self.tauon.gui.show_message(self.strings.spotify_need_enable) return [] albums = self.spotify.saved_albums() playlist = [] self.update_existing_import_list() self.cache_saved_albums.clear() pages = self.spotify.all_pages(albums) for page in pages: for a in page.items: self.load_album(a.album, playlist) if a.album.external_urls["spotify"] not in self.cache_saved_albums: self.cache_saved_albums.append(a.album.external_urls["spotify"]) if return_list: return playlist self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title=self.strings.spotify_albums, playlist=playlist)) self.tauon.pctl.gen_codes[self.tauon.pl_to_id(len(self.tauon.pctl.multi_playlist) - 1)] = "sal" self.spotify_com = False def append_track(self, url): self.connect() if not self.spotify: return if url.startswith("spotify:track:"): id = url[14:] else: url = url.split("?")[0] id = url.strip("/").split("/")[-1] track = self.spotify.track(id) tr = self.load_track(track) self.tauon.pctl.master_library[tr.index] = tr self.tauon.pctl.multi_playlist[self.tauon.pctl.active_playlist_viewing][2].append(tr.index) self.tauon.gui.pl_update += 1 def append_album(self, url, playlist_number=None, return_list=False): self.connect() if not self.spotify: return if url.startswith("spotify:album:"): id = url[14:] else: url = url.split("?")[0] id = url.strip("/").split("/")[-1] album = self.spotify.album(id) playlist = [] self.update_existing_import_list() self.load_album(album, playlist) if return_list: return playlist if playlist_number is None: playlist_number = self.tauon.pctl.active_playlist_viewing self.tauon.pctl.multi_playlist[playlist_number][2].extend(playlist) self.tauon.gui.pl_update += 1 def playlist(self, url, return_list=False, silent=False): self.connect() if not self.spotify: return [] if url.startswith("spotify:playlist:"): id = url[17:] else: url = url.split("?")[0] if len(url) != 22: id = url.strip("/").split("/")[-1] else: id = url if len(id) != 22: print("ID Error") if return_list: return [] return p = self.spotify.playlist(id) playlist = [] self.update_existing_import_list() pages = self.spotify.all_pages(p.tracks) for page in pages: for item in page.items: nt = self.load_track(item.track, include_album_url=True) self.tauon.pctl.master_library[nt.index] = nt playlist.append(nt.index) if return_list: return playlist title = p.name + " by " + p.owner.display_name self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title=title, playlist=playlist)) if p.name == "Discover Weekly" or p.name == "Release Radar": self.tauon.pctl.multi_playlist[len(self.tauon.pctl.multi_playlist) - 1][4] = 1 self.tauon.pctl.gen_codes[self.tauon.pl_to_id(len(self.tauon.pctl.multi_playlist) - 1)] = f"spl\"{id}\"" if not silent: self.tauon.switch_playlist(len(self.tauon.pctl.multi_playlist) - 1) def artist_playlist(self, url): id = url.strip("/").split("/")[-1] artist = self.spotify.artist(id) artist_albums = self.spotify.artist_albums(id, limit=50, include_groups=["album"]) playlist = [] self.update_existing_import_list() for a in artist_albums.items: full_album = self.spotify.album(a.id) self.load_album(full_album, playlist) self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title="Spotify: " + artist.name, playlist=playlist)) self.tauon.switch_playlist(len(self.tauon.pctl.multi_playlist) - 1) self.tauon.gui.message_box = False def update_existing_import_list(self): self.current_imports.clear() for tr in self.tauon.pctl.master_library.values(): if "spotify-track-url" in tr.misc: self.current_imports[tr.misc["spotify-track-url"]] = tr def create_playlist(self, name): print("Create new spotify playlist") self.connect() if not self.spotify: return None try: user = self.spotify.current_user() playlist = self.spotify.playlist_create(user.id, name, True) return playlist.id except: return None def upload_playlist(self, playlist_id, track_urls): self.connect() if not self.spotify: return None try: uris = [] for url in track_urls: uris.append("spotify:track:" + url.strip("/").split("/")[-1]) self.spotify.playlist_clear(playlist_id) time.sleep(0.05) with self.spotify.chunked(True): self.spotify.playlist_add(playlist_id, uris) except: self.tauon.gui.show_message("Spotify upload error!", mode="error") def load_album(self, album, playlist): #a = item album_url = album.external_urls["spotify"] art_url = album.images[0].url album_name = album.name total_tracks = album.total_tracks date = album.release_date album_artist = album.artists[0].name id = album.id parent = (album_artist + " - " + album_name).strip("- ") # print(a.release_date, a.name) for track in album.tracks.items: pr = None if "spotify" in track.external_urls: pr = self.current_imports.get(track.external_urls["spotify"]) if pr: new = False nt = pr else: new = True nt = self.tauon.TrackClass() nt.index = self.tauon.pctl.master_count nt.is_network = True nt.file_ext = "SPTY" nt.url_key = track.id if track.artists and "spotify" in track.artists[0].external_urls: nt.misc["spotify-artist-url"] = track.artists[0].external_urls["spotify"] nt.misc["spotify-album-url"] = album_url if "spotify" in track.external_urls: nt.misc["spotify-track-url"] = track.external_urls["spotify"] nt.artist = track.artists[0].name nt.album_artist = album_artist nt.date = date nt.album = album_name nt.disc_number = track.disc_number #nt.disc_total = nt.length = track.duration_ms / 1000 nt.title = track.name nt.track_number = track.track_number nt.track_total = total_tracks nt.art_url_key = art_url nt.parent_folder_path = parent nt.parent_folder_name = parent if new: self.tauon.pctl.master_count += 1 self.tauon.pctl.master_library[nt.index] = nt playlist.append(nt.index) def load_track(self, track, update_master_count=True, include_album_url=False): if "spotify" in track.external_urls: pr = self.current_imports.get(track.external_urls["spotify"]) else: pr = False if pr: new = False nt = pr else: new = True nt = self.tauon.TrackClass() nt.index = self.tauon.pctl.master_count nt.is_network = True nt.file_ext = "SPTY" nt.url_key = track.id #if new: if "spotify" in track.artists[0].external_urls: nt.misc["spotify-artist-url"] = track.artists[0].external_urls["spotify"] if include_album_url and "spotify-album-url" not in nt.misc: if "spotify" in track.album.external_urls: nt.misc["spotify-album-url"] = track.album.external_urls["spotify"] if "spotify" in track.external_urls: nt.misc["spotify-track-url"] = track.external_urls["spotify"] if track.artists[0].name: nt.artist = track.artists[0].name if track.album.artists: nt.album_artist = track.album.artists[0].name if track.album.release_date: nt.date = track.album.release_date nt.album = track.album.name nt.disc_number = track.disc_number nt.length = track.duration_ms / 1000 nt.title = track.name nt.track_number = track.track_number # nt.track_total = total_tracks if track.album.images: nt.art_url_key = track.album.images[0].url parent = (nt.album_artist + " - " + nt.album).strip("- ") nt.parent_folder_path = parent nt.parent_folder_name = parent if update_master_count and new: self.tauon.pctl.master_count += 1 return nt def like_track(self, tract_object): track_url = tract_object.misc.get("spotify-track-url", False) if track_url: id = track_url.strip("/").split("/")[-1] results = self.spotify.saved_tracks_contains([id]) if not results or results[0] is False: self.spotify.saved_tracks_add([id]) tract_object.misc["spotify-liked"] = True self.tauon.gui.show_message(self.strings.spotify_like_added, mode="done") return self.tauon.gui.show_message(self.strings.spotify_already_liked) return def unlike_track(self, tract_object): track_url = tract_object.misc.get("spotify-track-url", False) if track_url: id = track_url.strip("/").split("/")[-1] results = self.spotify.saved_tracks_contains([id]) if not results or results[0] is True: self.spotify.saved_tracks_delete([id]) tract_object.misc.pop("spotify-liked", None) self.tauon.gui.show_message(self.strings.spotify_un_liked, mode="done") return self.tauon.gui.show_message(self.strings.spotify_already_un_liked) return def get_library_likes(self, return_list=False): self.connect() if not self.spotify: self.spotify_com = False self.tauon.gui.show_message(self.strings.spotify_need_enable) return [] self.update_existing_import_list() tracks = self.spotify.saved_tracks() playlist = [] for tr in self.tauon.pctl.master_library.values(): tr.misc.pop("spotify-liked", None) pages = self.spotify.all_pages(tracks) for page in pages: for item in page.items: nt = self.load_track(item.track) self.tauon.pctl.master_library[nt.index] = nt playlist.append(nt.index) nt.misc["spotify-liked"] = True if return_list: return playlist for p in self.tauon.pctl.multi_playlist: if p[0] == self.tauon.strings.spotify_likes: p[2][:] = playlist[:] self.spotify_com = False return self.tauon.pctl.multi_playlist.append(self.tauon.pl_gen(title=self.tauon.strings.spotify_likes, playlist=playlist)) self.tauon.pctl.gen_codes[self.tauon.pl_to_id(len(self.tauon.pctl.multi_playlist) - 1)] = "slt" self.spotify_com = False def monitor(self): tr = self.tauon.pctl.playing_object() result = None # Detect if playback has resumed if self.playing and self.paused: result = self.spotify.playback_currently_playing() if result and result.is_playing: self.paused = False self.progress_timer.set() self.tauon.pctl.playing_state = 1 self.tauon.gui.update += 1 # Detect is playback has been modified elif self.playing and self.start_timer.get() > 4 and self.tauon.pctl.playing_time + 5 < tr.length: if not result: result = self.spotify.playback_currently_playing() # Playback has been stopped? if (result is None or result.item is None) or tr is None: self.playing = False self.tauon.pctl.stop() return # Playback has been paused? elif tr and result and not result.is_playing: self.paused = True self.tauon.pctl.playing_state = 2 self.tauon.gui.update += 1 return # Something else is now playing? If so, switch into coast mode if result.item.name != tr.title: self.tauon.pctl.playing_state = 3 self.playing = False self.coasting = True self.coast_update(result) self.tauon.gui.pl_update += 2 return p = result.progress_ms if p is not None: #if abs(self.tauon.pctl.playing_time - (p / 1000)) > 0.4: # print("DESYNC") # print(abs(self.tauon.pctl.playing_time - (p / 1000))) self.tauon.pctl.playing_time = p / 1000 self.tauon.pctl.decode_time = self.tauon.pctl.playing_time # else: # print("SYNCED") def update(self, start=False): if self.playing: self.coasting = False return self.connect() if not self.spotify: return result = self.spotify.playback_currently_playing() if self.playing or (not self.coasting and not start): return try: self.tauon.tm.player_lock.release() except: pass if result is None or result.is_playing is False: if self.coasting: if self.tauon.pctl.radio_image_bin: self.loaded_art = "" self.tauon.pctl.radio_image_bin.close() self.tauon.pctl.radio_image_bin = None self.tauon.dummy_track.artist = "" self.tauon.dummy_track.date = "" self.tauon.dummy_track.title = "" self.tauon.dummy_track.album = "" self.tauon.dummy_track.art_url_key = "" self.tauon.gui.clear_image_cache_next = True self.paused = True else: self.tauon.gui.show_message(self.strings.spotify_not_playing) return self.coasting = True self.started_once = True self.tauon.pctl.playing_state = 3 if result.is_playing: self.paused = False else: self.paused = True self.coast_update(result) def append_playing(self, playlist_number): if not self.coasting: return tr = self.tauon.pctl.playing_object() if tr and "spotify-album-url" in tr.misc: self.append_album(tr.misc["spotify-album-url"], playlist_number) def coast_update(self, result): if result is None or result.item is None: print("Spotify returned unknown") return self.tauon.dummy_track.artist = result.item.artists[0].name self.tauon.dummy_track.title = result.item.name self.tauon.dummy_track.album = result.item.album.name self.tauon.dummy_track.date = result.item.album.release_date self.tauon.dummy_track.file_ext = "Spotify" self.progress_timer.set() self.update_timer.set() d = result.item.duration_ms if d is not None: self.tauon.pctl.playing_length = d / 1000 p = result.progress_ms if p is not None: self.tauon.pctl.playing_time = p / 1000 self.tauon.pctl.decode_time = self.tauon.pctl.playing_time art_url = result.item.album.images[0].url self.tauon.dummy_track.url_key = result.item.id self.tauon.dummy_track.misc["spotify-album-url"] = result.item.album.external_urls["spotify"] self.tauon.dummy_track.misc["spotify-track-url"] = result.item.external_urls["spotify"] if art_url and self.loaded_art != art_url: self.loaded_art = art_url art_response = requests.get(art_url) if self.tauon.pctl.radio_image_bin: self.tauon.pctl.radio_image_bin.close() self.tauon.pctl.radio_image_bin = None self.tauon.pctl.radio_image_bin = io.BytesIO(art_response.content) self.tauon.pctl.radio_image_bin.seek(0) self.tauon.dummy_track.art_url_key = "ok" self.tauon.gui.clear_image_cache_next = True self.tauon.gui.update += 2 self.tauon.gui.pl_update += 1
class Enc: def __init__(self): self.encoder = None self.decoder = None self.raw_buffer = io.BytesIO() self.raw_buffer_size = 0 self.output_buffer = io.BytesIO() self.output_buffer_size = 0 self.temp_buffer = io.BytesIO() self.temp_buffer_size = 0 self.stream_time = Timer() self.bytes_sent = 0 self.track_bytes_sent = 0 self.dry = 0 def get_decode_command(self, target, start): s = start s, ms = divmod(s, 1) m, ss = divmod(s, 60) hh, mm = divmod(m, 60) ms *= 10 t = f"{str(int(hh)).zfill(2)}:{str(int(mm)).zfill(2)}:{str(int(ss)).zfill(2)}.{str(round(ms))}" return [ 'ffmpeg', "-i", target, "-ss", t, "-acodec", "pcm_s16le", "-f", "s16le", "-ac", "2", "-ar", # -re "48000", "-" ] def main(self): while True: if not pctl.broadcast_active: time.sleep(0.1) if pctl.broadcastCommandReady: command = pctl.broadcastCommand pctl.playerCommand = "" pctl.broadcastCommandReady = False if command == "encstop": self.decoder.terminate() self.encoder.terminate() self.encoder = None self.decoder = None self.track_bytes_sent = 0 self.output_buffer_size = 0 self.raw_buffer = io.BytesIO() self.output_buffer = io.BytesIO() tauon.chunker.chunks.clear() tauon.chunker.headers.clear() tauon.chunker.master_count = 0 pctl.broadcast_active = False pctl.broadcast_time = 0 if command == "encstart": target = pctl.target_open print(f"URI = {target}") pctl.broadcast_active = True print("Start encoder") #cmd = shlex.split("opusenc --raw --raw-rate 48000 - -") cmd = [ "ffmpeg", "-f", "s16le", "-ar", "48000", "-ac", "2", "-i", "pipe:0", '-f', "opus", "-c:a", "libopus", "pipe:1" ] # cmd = shlex.split("oggenc --raw --raw-rate 48000 -") self.encoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) fcntl.fcntl(self.encoder.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) print("Begin decode of file") cmd = self.get_decode_command(target, pctl.b_start_time) print(cmd) self.decoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) time.sleep(0.1) self.stream_time.force_set(6) print("Broadcast started") if command == "cast-next": target = pctl.target_open print(f"URI = {target}") self.decoder.terminate() cmd = self.get_decode_command(target, 0) self.decoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) time.sleep(0.1) print("started next") self.track_bytes_sent = 0 if command == "encseek": target = pctl.target_open start = pctl.b_start_time + pctl.broadcast_seek_position print(f"URI = {target}") self.decoder.terminate() cmd = self.get_decode_command(target, start) self.decoder = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) time.sleep(0.1) print("started next") self.track_bytes_sent = pctl.broadcast_seek_position * ( 48000 * (16 / 8) * 2) if self.decoder: pctl.broadcast_time = self.track_bytes_sent / (48000 * (16 / 8) * 2) st = self.stream_time.get() ss = self.bytes_sent / (48000 * (16 / 8) * 2) #print((st, ss)) if ss < st: # We owe: owed_seconds = st - ss owed_bytes = owed_seconds * (48000 * (16 / 8) * 2) while owed_bytes > 0: #print("PUMP") # Pump data out of decoder data = self.decoder.stdout.read( int(48000 * (16 / 8) * 2)) #print(data) if not data: self.dry += 1 if owed_seconds > 0.1 and self.dry > 2: #print("SILENCE") data = b"\x00" * 19200 else: break else: self.dry = 0 self.raw_buffer.write(data) self.raw_buffer_size += len(data) self.bytes_sent += len(data) self.track_bytes_sent += len(data) owed_bytes -= len(data) break if not data: #print("No more decoded data...") time.sleep(0.01) # Push data into encoder if self.raw_buffer_size > 0: self.raw_buffer.seek(0) data = self.raw_buffer.read(self.raw_buffer_size) self.encoder.stdin.write(data) # Reset the buffer self.raw_buffer_size = 0 self.raw_buffer.seek(0) # Receive encoded data data = self.encoder.stdout.read() if data: #print("WRITE") self.output_buffer.write(data) self.output_buffer_size += len(data) # Split OGG pages if self.output_buffer_size > 12000: self.output_buffer.seek(6) gp = self.output_buffer.read(8) gp = int.from_bytes(gp, 'big') self.output_buffer.seek(26) cont = self.output_buffer.read(1) cont = int.from_bytes(cont, 'big') #print(f"{cont} segments") total = cont while cont: value = self.output_buffer.read(1) value = int.from_bytes(value, 'big') #print(f"value {value}") cont -= 1 total += value total = total + 27 self.output_buffer.seek(0, 2) # self.output_buffer.seek(0) # print(self.output_buffer.read(4)) # self.output_buffer.seek(0, 2) if self.output_buffer_size >= total: # Extract the first complete page self.output_buffer.seek(0) page = self.output_buffer.read(total) # Save the page if gp == 0: tauon.chunker.headers.append(page) else: tauon.chunker.chunks[ tauon.chunker.master_count] = page tauon.chunker.master_count += 1 d = tauon.chunker.master_count - 30 if d > 1: del tauon.chunker.chunks[d] # print(f"Received page {tauon.chunker.master_count}") # Reset the buffer with the remainder self.temp_buffer.seek(0) self.temp_buffer.write(self.output_buffer.read()) self.temp_buffer.seek(0) del self.output_buffer self.output_buffer = io.BytesIO() self.output_buffer.seek(0) self.output_buffer.write(self.temp_buffer.read()) self.output_buffer_size = self.output_buffer.tell() del self.temp_buffer self.temp_buffer = io.BytesIO()