コード例 #1
0
def GetLikeStatus(ytmusic: YTMusic,
                  videoId: str,
                  browseId: str = None,
                  db: Database = None,
                  verbose: bool = False) -> LikeStatus:
    likeStatus = None
    try:
        if db:
            songDocument = db['songs'].find_one({"ytmusicId": videoId},
                                                projection={"likeStatus": 1})
            if "likeStatus" in songDocument:
                likeStatus = LikeStatus(songDocument['likeStatus'])
                if verbose:
                    print(f"GetLikeStatus: Scrobble likeStatus {likeStatus}")
        if likeStatus is None:
            if browseId is None:
                songData = GetDetailedSongData(ytmusic, videoId)
                if "album" in songData:
                    browseId = songData['album']['id']
                elif "likeStatus" in songData:
                    likeStatus = LikeStatus[songData['likeStatus']]
                    if verbose:
                        print(
                            f"GetLikeStatus: YTMusic likeStatus {likeStatus}")
            if browseId:
                albumData = ytmusic.get_album(browseId)
                tracks = [
                    track for track in albumData['tracks']
                    if track['videoId'] == videoId
                ]
                likeStatus = LikeStatus[tracks[0]['likeStatus']]
            if db and likeStatus:
                result = db['songs'].find_one_and_update(
                    {"ytmusicId": videoId},
                    {"$set": {
                        "likeStatus": likeStatus.value
                    }})
                if verbose:
                    if result:
                        print(f"GetLikeStatus: Updated song {result['_id']}"
                              f" like status")
                    else:
                        print(
                            f"GetLikeStatus: Could not update song. Not found."
                        )
    except Exception as e:
        if verbose:
            print("GetLikeStatus Error: ", e)
        return
    return likeStatus
コード例 #2
0
class YoutubeMusicSkill(CommonPlaySkill):
    """Youtube Music."""
    def __init__(self):
        super(YoutubeMusicSkill, self).__init__()
        self.yt = YTMusic()
        self.idle_count = 0
        self.ducking = False
        self.mouth_text = None

        self._playlists = None
        self.saved_tracks = None
        self.regexes = {}
        self.last_played_type = None  # The last uri type that was started
        self.is_playing = False

    def translate_regex(self, regex):
        if regex not in self.regexes:
            path = self.find_resource(regex + '.regex')
            if path:
                with open(path) as f:
                    string = f.read().strip()
                self.regexes[regex] = string
        return self.regexes[regex]

    def initialize(self):
        super().initialize()
        # Setup handlers for playback control messages
        self.add_event('mycroft.audio.service.next', self.next_track)
        self.add_event('mycroft.audio.service.prev', self.prev_track)
        self.add_event('mycroft.audio.service.pause', self.pause)
        self.add_event('mycroft.audio.service.resume', self.resume)
        # Check and then monitor for credential changes
        self.settings_change_callback = self.on_websettings_changed
        self.on_websettings_changed()

    def on_websettings_changed(self):
        # Only attempt to load credentials if the username has been set
        # will limit the accesses to the api.
        self.load_credentials()

    def load_credentials(self):
        """Retrieve credentials from the backend and connect to Youtube."""
        pass

    def failed_auth(self):
        if 'user' not in self.settings:
            self.log.error('Settings hasn\'t been received yet')
            self.speak_dialog('NoSettingsReceived')
        elif not self.settings.get("user"):
            self.log.error('User info has not been set.')
            # Assume this is initial setup
            self.speak_dialog('NotConfigured')
        else:
            # Assume password changed or there is a typo
            self.log.error('User info has been set but Auth failed.')
            self.speak_dialog('NotAuthorized')

    ######################################################################
    # Handle auto ducking when listener is started.

    def handle_listener_started(self, message):
        """Handle auto ducking when listener is started.

        The ducking is enabled/disabled using the skill settings on home.

        TODO: Evaluate the Idle check logic
        """
        if (self.is_playing and self.settings.get('use_ducking', False)):
            self.__pause()
            self.ducking = True

            # Start idle check
            self.idle_count = 0
            self.cancel_scheduled_event('IdleCheck')
            self.schedule_repeating_event(self.check_for_idle,
                                          None,
                                          1,
                                          name='IdleCheck')

    def check_for_idle(self):
        """Repeating event checking for end of auto ducking."""
        if not self.ducking:
            self.cancel_scheduled_event('IdleCheck')
            return

        active = self.enclosure.display_manager.get_active()
        if not active == '' or active == 'YoutubeMusicSkill':
            # No activity, start to fall asleep
            self.idle_count += 1

            if self.idle_count >= 5:
                # Resume playback after 5 seconds of being idle
                self.cancel_scheduled_event('IdleCheck')
                self.ducking = False
                self.resume()
        else:
            self.idle_count = 0

    ######################################################################
    # Mycroft display handling

    def start_monitor(self):
        """Monitoring and current song display."""
        # Clear any existing event
        self.stop_monitor()

        # Schedule a new one every 5 seconds to monitor/update display
        self.schedule_repeating_event(self._update_display,
                                      None,
                                      5,
                                      name='MonitorYoutube')
        self.add_event('recognizer_loop:record_begin',
                       self.handle_listener_started)

    def stop_monitor(self):
        # Clear any existing event
        self.cancel_scheduled_event('MonitorYoutube')

    def _update_display(self, message):
        # Checks once a second for feedback
        pass

    def CPS_match_query_phrase(self, phrase):
        """Handler for common play framework Query."""

        youtube_specified = 'youtube' in phrase
        bonus = 0.1 if youtube_specified else 0.0
        phrase = re.sub(self.translate_regex('on_youtube'), '', phrase)

        confidence, data = self.continue_playback(phrase, bonus)
        if not data:
            confidence, data = self.specific_query(phrase, bonus)
            if not data:
                confidence, data = self.generic_query(phrase, bonus)

        if data:
            self.log.info('Youtube Music confidence: {}'.format(confidence))
            self.log.info('                    data: {}'.format(data))

            if data.get('type') in [
                    'saved_tracks', 'album', 'artist', 'track', 'playlist',
                    'show'
            ]:
                if youtube_specified:
                    # " play great song on youtube'
                    level = CPSMatchLevel.EXACT
                else:
                    if confidence > 0.9:
                        # TODO: After 19.02 scoring change
                        # level = CPSMatchLevel.MULTI_KEY
                        level = CPSMatchLevel.TITLE
                    elif confidence < 0.5:
                        level = CPSMatchLevel.GENERIC
                    else:
                        level = CPSMatchLevel.TITLE
                    phrase += ' on youtube'
            elif data.get('type') == 'continue':
                if youtube_specified > 0:
                    # "resume playback on youtube"
                    level = CPSMatchLevel.EXACT
                else:
                    # "resume playback"
                    level = CPSMatchLevel.GENERIC
                    phrase += ' on youtube'
            else:
                self.log.warning('Unexpected youtube type: '
                                 '{}'.format(data.get('type')))
                level = CPSMatchLevel.GENERIC

            return phrase, level, data
        else:
            self.log.debug('Couldn\'t find anything to play on Youtube')

    def continue_playback(self, phrase, bonus):
        if phrase.strip() == 'youtube':
            return (1.0, {'data': None, 'name': None, 'type': 'continue'})
        else:
            return NOTHING_FOUND

    def specific_query(self, phrase, bonus):
        """
        Check if the phrase can be matched against a specific youtube request.

        This includes asking for saved items, playlists, albums, podcasts,
        artists or songs.

        Arguments:
            phrase (str): Text to match against
            bonus (float): Any existing match bonus

        Returns: Tuple with confidence and data or NOTHING_FOUND
        """
        # Check if playlist
        #match = re.match(self.translate_regex('playlist'), phrase)
        #if match:
        #    return self.query_playlist(match.groupdict()['playlist'])

        # Check album
        match = re.match(self.translate_regex('album'), phrase)
        if match:
            bonus += 0.1
            album = match.groupdict()['album']
            return self.query_album(album, bonus)

        # Check artist
        match = re.match(self.translate_regex('artist'), phrase)
        if match:
            artist = match.groupdict()['artist']
            return self.query_artist(artist, bonus)
        match = re.match(self.translate_regex('song'), phrase)
        if match:
            song = match.groupdict()['track']
            return self.query_song(song, bonus)

        # Check if podcast
        #match = re.match(self.translate_regex('podcast'), phrase)
        #if match
        #    return self.query_show(match.groupdict()['podcast'])

        return NOTHING_FOUND

    def generic_query(self, phrase, bonus):
        """Check for a generic query, not asking for any special feature.

        This will try to parse the entire phrase in the following order
        - As a user playlist
        - As an album
        - As a track
        - As a public playlist

        Arguments:
            phrase (str): Text to match against
            bonus (float): Any existing match bonus

        Returns: Tuple with confidence and data or NOTHING_FOUND
        """
        self.log.info('Handling "{}" as a genric query...'.format(phrase))
        results = []

        # Check for artist
        self.log.info('Checking artists')
        conf, data = self.query_artist(phrase, bonus)
        if conf and conf > DIRECT_RESPONSE_CONFIDENCE:
            return conf, data
        elif conf and conf > MATCH_CONFIDENCE:
            results.append((conf, data))

        # Check for track
        self.log.info('Checking tracks')
        conf, data = self.query_song(phrase, bonus)
        if conf and conf > DIRECT_RESPONSE_CONFIDENCE:
            return conf, data
        elif conf and conf > MATCH_CONFIDENCE:
            results.append((conf, data))

        # Check for album
        self.log.info('Checking albums')
        conf, data = self.query_album(phrase, bonus)
        if conf and conf > DIRECT_RESPONSE_CONFIDENCE:
            return conf, data
        elif conf and conf > MATCH_CONFIDENCE:
            results.append((conf, data))

        # Check for public playlist
        self.log.info('Checking tracks')
        conf, data = self.get_best_public_playlist(phrase)
        if conf and conf > DIRECT_RESPONSE_CONFIDENCE:
            return conf, data
        elif conf and conf > MATCH_CONFIDENCE:
            results.append((conf, data))

        return best_result(results)

    def query_artist(self, artist, bonus=0.0):
        """Try to find an artist.

        Arguments:
            artist (str): Artist to search for
            bonus (float): Any bonus to apply to the confidence

        Returns: Tuple with confidence and data or NOTHING_FOUND
        """
        bonus += 0.1
        data = self.yt.search(artist, 'artists')
        if data:
            from pprint import pprint
            pprint(data[0])
            best = data[0]['artist']
            confidence = fuzzy_match(best, artist.lower()) + bonus
            confidence = min(confidence, 1.0)
            return (confidence, {
                'browseId': data[0]['browseId'],
                'name': None,
                'type': 'artist'
            })
        else:
            return NOTHING_FOUND

    def query_album(self, album, bonus):
        """Try to find an album.

        Searches Youtube Music  by album and artist if available.

        Arguments:
            album (str): Album to search for
            bonus (float): Any bonus to apply to the confidence

        Returns: Tuple with confidence and data or NOTHING_FOUND
        """
        self.log.info('CHECKING FOR ALBUMS!')
        data = None
        by_word = ' {} '.format(self.translate('by'))
        if len(album.split(by_word)) > 1:
            album, artist = album.split(by_word)
            album_search = '{} {}'.format(artist, album)
            bonus += 0.1
        else:
            album_search = album

        res = self.yt.search(album_search, 'albums')
        if res:
            self.log.info(res)
            return (0.7, {'type': 'album', 'browseId': res[0]['browseId']})
        else:
            return NOTHING_FOUND

    def get_best_user_playlist(self, playlist):
        return NOTHING_FOUND

    def get_best_public_playlist(self, playlist):
        return NOTHING_FOUND

    def query_playlist(self, playlist):
        """Try to find a playlist.

        First searches the users playlists, then tries to find a public
        one.

        Arguments:
            playlist (str): Playlist to search for

        Returns: Tuple with confidence and data or NOTHING_FOUND
        """
        result, conf = self.get_best_user_playlist(playlist)
        if playlist and conf > 0.5:
            uri = self.playlists[result]
            return (conf, {'data': uri, 'name': playlist, 'type': 'playlist'})
        else:
            return self.get_best_public_playlist(playlist)

    def query_song(self, song, bonus):
        """Try to find a song.

        Searches Youtube Music for song and artist if provided.

        Arguments:
            song (str): Song to search for
            bonus (float): Any bonus to apply to the confidence

        Returns: Tuple with confidence and data or NOTHING_FOUND
        """
        data = None
        by_word = ' {} '.format(self.translate('by'))
        if len(song.split(by_word)) > 1:
            song, artist = song.split(by_word)
            song_search = '{} {}'.format(artist, song)
        else:
            song_search = song

        # Get song from youtube
        res = self.yt.search(song_search, 'songs')
        if res:
            self.log.info(res[0])
            return (0.7, {'type': 'song', 'videoId': res[0]['videoId']})
        else:
            return NOTHING_FOUND

        return None

    def CPS_start(self, phrase, data):
        """Handler for common play framework start playback request."""
        try:
            if data['type'] == 'continue':
                self.acknowledge()
                self.continue_current_playlist()
            elif data['type'] == 'playlist':
                self.play_playlist(data['playlistId'])
            elif data['type'] == 'artist':
                self.play_artist(data['browseId'])
            elif data['type'] == 'album':
                self.play_album(data['browseId'])
            elif data['type'] == 'song':
                self.play_video_list([data['videoId']])
            self.enable_playing_intents()
            if data.get('type') and data['type'] != 'continue':
                self.last_played_type = data['type']
            self.is_playing = True

        except PlaylistNotFoundError:
            self.speak_dialog('PlaybackFailed',
                              {'reason': self.translate('PlaylistNotFound')})
        except Exception as e:
            self.log.exception(str(e))
            self.speak_dialog('PlaybackFailed', {'reason': str(e)})

    def play_playlist(self, playlist_id):
        self.log.info('Playlist playback not implemented!!!')

    def play_album(self, browse_id):
        album = self.yt.get_album(browse_id)
        tracks = [t['videoId'] for t in album['tracks']]
        self.log.info('playing album!')
        self.log.info(tracks)
        self.play_video_list(tracks)

    def play_artist(self, browse_id):
        artist = self.yt.get_artist(browse_id)
        tracks = [t['videoId'] for t in artist['songs']['results']]
        self.play_video_list(tracks)

    def play_video_list(self, v_list):
        url = 'https://www.youtube.com/watch?v={}'
        if 'vlc' in self.audioservice.available_backends():
            self.log.info('Using vlc backend!')
            trx = [pafy.new(url.format(v)).getbestaudio().url for v in v_list]
            self.audioservice.play(trx, utterance='vlc')
        else:
            v = pafy.new(url.format(v_list[0]))
            audio = v.getbestaudio()
            cache = '/tmp/{}.mp3'.format(v_list[0])
            subprocess.call(['ffmpeg', '-i', audio.url, cache])
            self.audioservice.play(cache)

    def create_intents(self):
        """Setup the intents."""
        intent = IntentBuilder('').require('Youtube').require('Search') \
                                  .require('For')
        #self.register_intent(intent, self.search_youtube)
        self.register_intent_file('ShuffleOn.intent', self.shuffle_on)
        self.register_intent_file('ShuffleOff.intent', self.shuffle_off)
        self.register_intent_file('WhatSong.intent', self.song_info)
        self.register_intent_file('WhatAlbum.intent', self.album_info)
        self.register_intent_file('WhatArtist.intent', self.artist_info)
        self.register_intent_file('StopMusic.intent', self.handle_stop)
        time.sleep(0.5)
        self.disable_playing_intents()

    def enable_playing_intents(self):
        self.enable_intent('WhatSong.intent')
        self.enable_intent('WhatAlbum.intent')
        self.enable_intent('WhatArtist.intent')
        self.enable_intent('StopMusic.intent')

    def disable_playing_intents(self):
        self.disable_intent('WhatSong.intent')
        self.disable_intent('WhatAlbum.intent')
        self.disable_intent('WhatArtist.intent')
        self.disable_intent('StopMusic.intent')

    def shuffle_on(self):
        """ Turn on shuffling """
        pass

    def shuffle_off(self):
        """ Turn off shuffling """
        pass

    def song_info(self, message):
        """ Speak song info. """
        pass

    def album_info(self, message):
        """Speak album info."""
        pass

    def artist_info(self, message):
        """Speak artist info."""
        pass

    def __pause(self):
        self.audioservice.pause()

    def pause(self, message=None):
        """ Handler for playback control pause. """
        pass

    def resume(self, message=None):
        """ Handler for playback control resume. """
        pass

    def next_track(self, message):
        """ Handler for playback control next. """
        pass

    def prev_track(self, message):
        """Handler for playback control prev."""
        pass

    def handle_stop(self, message):
        self.bus.emit(Message('mycroft.stop'))

    def do_stop(self):
        self.pause(None)

    def stop(self):
        """ Stop playback. """
        if self.is_playing:
            self.do_stop()
            return True
        else:
            return False

    def shutdown(self):
        """ Remove the monitor at shutdown. """
        self.stop_monitor()