Пример #1
0
    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'
Пример #2
0
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"
                                 })
Пример #3
0
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
Пример #4
0
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
Пример #5
0
    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',
                "-loglevel",
                "quiet",
                "-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":
                        # print("Stopping broadcast...")
                        pctl.broadcast_active = False
                        time.sleep(1)
                        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()
                        self.temp_buffer = io.BytesIO()
                        self.dry = 0
                        self.bytes_sent = 0
                        self.stream_time.set()
                        tauon.chunker.chunks.clear()
                        tauon.chunker.headers.clear()
                        tauon.chunker.master_count = 0
                        print("Broadcast stopped")

                        pctl.broadcast_time = 0

                    if command == "encstart":
                        # print("Start broadcast...")
                        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", "-loglevel", "quiet", "-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()