Exemple #1
0
    def __init__(self, args):
        threading.Thread.__init__(self)

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args

        # initially logged-out
        self.logged_out.set()

        config = spotify.Config()

        self.post = PostActions(args, self)
        self.web = WebAPI(args, self)

        proxy = os.environ.get('http_proxy')
        if proxy is not None:
            config.proxy = proxy

        # Application key
        if not path_exists(settings_dir()):
            os.makedirs(enc_str(settings_dir()))

        app_key_path = os.path.join(settings_dir(), "spotify_appkey.key")
        if not path_exists(app_key_path):
            print("\n" + Fore.RED + "Please copy your spotify_appkey.key to " + settings_dir() + Fore.RESET)
            sys.exit(1)

        config.load_application_key_file(app_key_path)
        config.settings_location = settings_dir()
        config.cache_location = settings_dir()

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        # disable scrobbling
        self.session.social.set_scrobbling(spotify.SocialProvider.SPOTIFY, spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(spotify.SocialProvider.FACEBOOK, spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(spotify.SocialProvider.LASTFM, spotify.ScrobblingState.LOCAL_DISABLED)

        bit_rates = dict([('160', BitRate.BITRATE_160K), ('320', BitRate.BITRATE_320K), ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED, self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK, self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY, self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST, self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in)

        self.event_loop = EventLoop(self.session, 0.1, self)
    def __init__(self, args):
        threading.Thread.__init__(self)

        # set to a daemon thread
        self.daemon = True

        # initalize progress meter
        self.progress = Progress(args, self)

        self.args = args
        self.logged_in = threading.Event()
        self.logged_out = threading.Event()
        self.logged_out.set()

        config = spotify.Config()

        default_dir = default_settings_dir()

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print(
                    "\n"
                    + Fore.YELLOW
                    + "Please copy your spotify_appkey.key to "
                    + default_dir
                    + ", or use the --key|-k option"
                    + Fore.RESET
                )
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)

        bit_rates = dict([("160", BitRate.BITRATE_160K), ("320", BitRate.BITRATE_320K), ("96", BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED, self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK, self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY, self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST, self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in)

        self.event_loop = spotify.EventLoop(self.session)
        self.event_loop.start()
Exemple #3
0
    def __init__(self, args):
        threading.Thread.__init__(self)

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args

        # initially logged-out
        self.logged_out.set()

        config = spotify.Config()
        default_dir = default_settings_dir()

        self.post = PostActions(args, self)

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print("\n" + Fore.YELLOW +
                      "Please copy your spotify_appkey.key to " + default_dir +
                      ", or use the --key|-k option" + Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        # disable scrobbling
        self.session.social.set_scrobbling(
            spotify.SocialProvider.SPOTIFY,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.FACEBOOK,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.LASTFM,
            spotify.ScrobblingState.LOCAL_DISABLED)

        bit_rates = dict([('160', BitRate.BITRATE_160K),
                          ('320', BitRate.BITRATE_320K),
                          ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
                        self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
                        self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
                        self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
                        self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in)

        self.event_loop = EventLoop(self.session, 0.1, self)
Exemple #4
0
class Ripper(threading.Thread):
    name = 'SpotifyRipperThread'

    audio_file = None
    pcm_file = None
    wav_file = None
    rip_proc = None
    pipe = None
    current_playlist = None
    current_album = None

    idx_digits = 3
    login_success = False
    progress = None
    sync = None
    post = None
    dev_null = None

    rip_queue = queue.Queue()

    # threading events
    logged_in = threading.Event()
    logged_out = threading.Event()
    ripping = threading.Event()
    end_of_track = threading.Event()
    finished = threading.Event()
    abort = threading.Event()

    def __init__(self, args):
        threading.Thread.__init__(self)

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args

        # initially logged-out
        self.logged_out.set()

        config = spotify.Config()
        default_dir = default_settings_dir()

        self.post = PostActions(args, self)

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print("\n" + Fore.YELLOW +
                      "Please copy your spotify_appkey.key to " + default_dir +
                      ", or use the --key|-k option" + Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        # disable scrobbling
        self.session.social.set_scrobbling(
            spotify.SocialProvider.SPOTIFY,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.FACEBOOK,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.LASTFM,
            spotify.ScrobblingState.LOCAL_DISABLED)

        bit_rates = dict([('160', BitRate.BITRATE_160K),
                          ('320', BitRate.BITRATE_320K),
                          ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
                        self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
                        self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
                        self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
                        self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in)

        self.event_loop = EventLoop(self.session, 0.1, self)

    def stop_event_loop(self):
        if self.event_loop.isAlive():
            self.event_loop.stop()
            self.event_loop.join()

    def run(self):
        args = self.args

        # start event loop
        self.event_loop.start()

        # login
        print("Logging in...")
        if args.last:
            self.login_as_last()
        elif args.user is not None and args.password is None:
            password = getpass.getpass()
            self.login(args.user[0], password)
        else:
            self.login(args.user[0], args.password[0])

        if not self.login_success:
            print(Fore.RED + "Encountered issue while logging into "
                  "Spotify, aborting..." + Fore.RESET)
            self.stop_event_loop()
            self.finished.set()
            return

        # check if we were passed a file name or search
        if len(args.uri) == 1 and os.path.exists(args.uri[0]):
            uris = [line.strip() for line in open(args.uri[0])]
        elif len(args.uri) == 1 and not args.uri[0].startswith("spotify:"):
            uris = [list(self.search_query(args.uri[0]))]
        else:
            uris = args.uri

        def get_tracks_from_uri(uri):
            if isinstance(uri, list):
                return uri
            else:
                if (args.exclude_appears_on
                        and uri.startswith("spotify:artist:")):
                    album_uris = self.load_artist_albums(uri)
                    return itertools.chain(*[
                        self.load_link(album_uri) for album_uri in album_uris
                    ])
                else:
                    return self.load_link(uri)

        # calculate total size and time
        all_tracks = []
        for uri in uris:
            tracks = get_tracks_from_uri(uri)
            all_tracks += list(tracks)

        self.progress.calc_total(all_tracks)

        if self.progress.total_size > 0:
            print("Total Download Size: " +
                  format_size(self.progress.total_size))

        # create track iterator
        for uri in uris:
            if self.abort.is_set():
                break

            tracks = list(get_tracks_from_uri(uri))

            if args.flat_with_index and self.current_playlist:
                self.idx_digits = len(str(len(self.current_playlist.tracks)))

            if args.playlist_sync and self.current_playlist:
                self.sync = Sync(args, self)
                self.sync.sync_playlist(self.current_playlist)

            # ripping loop
            for idx, track in enumerate(tracks):
                try:
                    if self.abort.is_set():
                        break

                    print('Loading track...')
                    track.load()
                    if track.availability != 1:
                        print(Fore.RED + 'Track is not available, '
                              'skipping...' + Fore.RESET)
                        self.post.log_failure(track)
                        continue

                    self.audio_file = self.format_track_path(idx, track)

                    if not args.overwrite and os.path.exists(self.audio_file):
                        print(Fore.YELLOW + "Skipping " + track.link.uri +
                              Fore.RESET)
                        print(Fore.CYAN + self.audio_file + Fore.RESET)
                        self.post.queue_remove_from_playlist(idx)
                        continue

                    self.session.player.load(track)
                    self.prepare_rip(idx, track)
                    self.session.player.play()

                    while not self.end_of_track.is_set() or \
                            not self.rip_queue.empty():
                        try:
                            if self.abort.is_set():
                                break

                            rip_item = self.rip_queue.get(timeout=1)
                            self.rip(self.session, rip_item[0], rip_item[1],
                                     rip_item[2])
                        except queue.Empty:
                            pass

                    if self.abort.is_set():
                        self.session.player.play(False)
                        self.end_of_track.set()
                        self.post.clean_up_partial()
                        self.post.log_failure(track)
                        break

                    self.end_of_track.clear()

                    self.finish_rip(track)

                    # update id3v2 with metadata and embed front cover image
                    set_metadata_tags(args, self.audio_file, track)

                    # make a note of the index and remove all the
                    # tracks from the playlist when everything is done
                    self.post.queue_remove_from_playlist(idx)

                except (spotify.Error, Exception) as e:
                    if isinstance(e, Exception):
                        print(Fore.RED + "Spotify error detected" + Fore.RESET)
                    print(str(e))
                    print("Skipping to next track...")
                    self.session.player.play(False)
                    self.post.clean_up_partial()
                    self.post.log_failure(track)
                    continue

            if not self.abort.is_set():
                # create playlist m3u file if needed
                self.post.create_playlist_m3u(tracks)

                # create playlist wpl file if needed
                self.post.create_playlist_wpl(tracks)

                # actually removing the tracks from playlist
                self.post.remove_tracks_from_playlist()

        # logout, we are done
        self.post.end_failure_log()
        self.post.print_summary()
        self.logout()
        self.stop_event_loop()
        self.finished.set()

    def load_link(self, uri):
        # blank out current playlist/album
        self.current_playlist = None
        self.current_album = None

        # ignore if the uri is just blank (e.g. from a file)
        if not uri:
            return iter([])

        link = self.session.get_link(uri)
        if link.type == spotify.LinkType.TRACK:
            track = link.as_track()
            return iter([track])
        elif link.type == spotify.LinkType.PLAYLIST:
            self.current_playlist = link.as_playlist()
            print('Loading playlist...')
            self.current_playlist.load()
            return iter(self.current_playlist.tracks)
        elif link.type == spotify.LinkType.STARRED:
            link_user = link.as_user()
            if link_user is not None:
                starred = self.session.get_starred(link_user.canonical_name)
            else:
                starred = self.session.get_starred()

            if starred is not None:
                print('Loading starred playlist...')
                starred.load()
                return iter(starred.tracks)
            else:
                print(Fore.RED + "Could not load starred playlist..." +
                      Fore.RESET)
                return iter([])
        elif link.type == spotify.LinkType.ALBUM:
            album = link.as_album()
            album_browser = album.browse()
            print('Loading album browser...')
            album_browser.load()
            self.current_album = album
            return iter(album_browser.tracks)
        elif link.type == spotify.LinkType.ARTIST:
            artist = link.as_artist()
            artist_browser = artist.browse()
            print('Loading artist browser...')
            artist_browser.load()
            return iter(artist_browser.tracks)
        return iter([])

    # excludes 'appears on' albums
    def load_artist_albums(self, uri):
        def get_albums_json(offset):
            url = 'https://api.spotify.com/v1/artists/' + \
                  uri_tokens[2] + \
                  '/albums/?=album_type=album,single,compilation' + \
                  '&limit=50&offset=' + str(offset)
            print(Fore.GREEN + "Attempting to retrieve albums "
                  "from Spotify's Web API" + Fore.RESET)
            print(Fore.CYAN + url + Fore.RESET)
            req = requests.get(url)
            if req.status_code == 200:
                return req.json()
            else:
                print(Fore.YELLOW + "URL returned non-200 HTTP code: " +
                      str(req.status_code) + Fore.RESET)
            return None

        # extract artist id from uri
        uri_tokens = uri.split(':')
        if len(uri_tokens) != 3:
            return []

        # it is possible we won't get all the albums on the first request
        offset = 0
        album_uris = []
        total = None
        while total is None or offset < total:
            try:
                # rate limit if not first request
                if total is None:
                    time.sleep(1.0)
                albums = get_albums_json(offset)
                if albums is None:
                    break

                # extract album URIs
                album_uris += [album['uri'] for album in albums['items']]
                offset = len(album_uris)
                if total is None:
                    total = albums['total']
            except KeyError as e:
                break
        print(str(len(album_uris)) + " albums found")
        return album_uris

    def search_query(self, query):
        args = self.args

        print("Searching for query: " + query)
        try:
            result = self.session.search(query)
            result.load()
        except spotify.Error as e:
            print(str(e))
            return iter([])

        # list tracks
        print(Fore.GREEN + "Results" + Fore.RESET)
        for track_idx, track in enumerate(result.tracks):
            print("  " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET + " [" +
                  to_ascii(args, track.album.name) + "] " +
                  to_ascii(args, track.artists[0].name) + " - " +
                  to_ascii(args, track.name) + " (" + str(track.popularity) +
                  ")")

        pick = raw_input("Pick track(s) (ex 1-3,5): ")

        def get_track(i):
            if i >= 0 and i < len(result.tracks):
                return iter([result.tracks[i]])
            return iter([])

        pattern = re.compile("^[0-9 ,\-]+$")
        if pick.isdigit():
            pick = int(pick) - 1
            return get_track(pick)
        elif pick.lower() == "a" or pick.lower() == "all":
            return iter(result.tracks)
        elif pattern.match(pick):

            def range_string(comma_string):
                def hyphen_range(hyphen_string):
                    x = [int(x) - 1 for x in hyphen_string.split('-')]
                    return range(x[0], x[-1] + 1)

                return itertools.chain(
                    *[hyphen_range(r) for r in comma_string.split(',')])

            picks = sorted(set(list(range_string(pick))))
            return itertools.chain(*[get_track(p) for p in picks])

        if pick != "":
            print(Fore.RED + "Invalid selection" + Fore.RESET)
        return iter([])

    def on_music_delivery(self, session, audio_format, frame_bytes,
                          num_frames):
        try:
            self.rip_queue.put_nowait(
                (audio_format.sample_rate, frame_bytes, num_frames))
        except queue.Full:
            print(Fore.RED + "rip_queue is full. dropped music data" +
                  Fore.RESET)
        return num_frames

    def on_connection_state_changed(self, session):
        if session.connection.state is spotify.ConnectionState.LOGGED_IN:
            self.login_success = True
            self.logged_in.set()
            self.logged_out.clear()
        elif session.connection.state is spotify.ConnectionState.LOGGED_OUT:
            self.logged_in.clear()
            self.logged_out.set()

    def on_logged_in(self, session, error):
        if error is spotify.ErrorType.OK:
            print("Logged in as " + session.user.display_name)
        else:
            error_map = {
                9: "CLIENT_TOO_OLD",
                8: "UNABLE_TO_CONTACT_SERVER",
                6: "BAD_USERNAME_OR_PASSWORD",
                7: "USER_BANNED",
                15: "USER_NEEDS_PREMIUM",
                16: "OTHER_TRANSIENT",
                10: "OTHER_PERMANENT"
            }
            print("Logged in failed: " +
                  error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error)))
            self.login_success = False
            self.logged_in.set()

    def play_token_lost(self, session):
        print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET)
        self.abort_rip()

    def on_end_of_track(self, session):
        self.session.player.play(False)
        self.end_of_track.set()

    def login(self, user, password):
        """login into Spotify"""
        self.session.login(user, password, remember_me=True)
        self.logged_in.wait()

    def login_as_last(self):
        """login as the previous logged in user"""
        try:
            self.session.relogin()
            self.logged_in.wait()
        except spotify.Error as e:
            self.login_success = False
            print(str(e))

    def logout(self):
        """logout from Spotify"""
        time.sleep(0.1)
        if self.logged_in.is_set():
            print('Logging out...')
            self.session.logout()
            self.logged_out.wait()

    def album_artists_web(self, uri):
        def get_album_json(album_id):
            url = 'https://api.spotify.com/v1/albums/' + album_id
            print(Fore.GREEN + "Attempting to retrieve album "
                  "from Spotify's Web API" + Fore.RESET)
            print(Fore.CYAN + url + Fore.RESET)
            req = requests.get(url)
            if req.status_code == 200:
                return req.json()
            else:
                print(Fore.YELLOW + "URL returned non-200 HTTP code: " +
                      str(req.status_code) + Fore.RESET)
            return None

        # extract album id from uri
        uri_tokens = uri.split(':')
        if len(uri_tokens) != 3:
            return None

        album = get_album_json(uri_tokens[2])
        if album is None:
            return None

        return [artist['name'] for artist in album['artists']]

    def format_track_path(self, idx, track):
        args = self.args
        _base_dir = base_dir(args)
        audio_file = args.format[0].strip()

        track_artist = to_ascii(args,
                                escape_filename_part(track.artists[0].name))
        track_artists = to_ascii(
            args, ", ".join([artist.name for artist in track.artists]))
        if len(track.artists) > 1:
            featuring_artists = to_ascii(
                args, ", ".join([artist.name for artist in track.artists[1:]]))
        else:
            featuring_artists = ""

        album_artist = to_ascii(
            args, self.current_album.artist.name
            if self.current_album is not None else track_artist)
        album_artists_web = track_artists

        # only retrieve album_artist_web if it exists in the format string
        if (self.current_album is not None
                and audio_file.find("{album_artists_web}") >= 0):
            artist_array = self.album_artists_web(self.current_album.link.uri)
            if artist_array is not None:
                album_artists_web = to_ascii(args, ", ".join(artist_array))

        album = to_ascii(args, escape_filename_part(track.album.name))
        track_name = to_ascii(args, escape_filename_part(track.name))
        year = str(track.album.year)
        extension = args.output_type
        idx_str = str(idx + 1)
        track_num = str(track.index)
        disc_num = str(track.disc)
        if self.current_playlist is not None:
            playlist_name = to_ascii(args, self.current_playlist.name)
            playlist_owner = to_ascii(args,
                                      self.current_playlist.owner.display_name)
        else:
            playlist_name = "No Playlist"
            playlist_owner = "No Playlist Owner"
        user = self.session.user.display_name

        tags = {
            "track_artist": track_artist,
            "track_artists": track_artists,
            "album_artist": album_artist,
            "album_artists_web": album_artists_web,
            "artist": track_artist,
            "artists": track_artists,
            "album": album,
            "track_name": track_name,
            "track": track_name,
            "year": year,
            "ext": extension,
            "extension": extension,
            "idx": idx_str,
            "index": idx_str,
            "track_num": track_num,
            "track_idx": track_num,
            "track_index": track_num,
            "disc_num": disc_num,
            "disc_idx": disc_num,
            "disc_index": disc_num,
            "playlist": playlist_name,
            "playlist_name": playlist_name,
            "playlist_owner": playlist_owner,
            "playlist_user": playlist_owner,
            "playlist_username": playlist_owner,
            "user": user,
            "username": user,
            "feat_artists": featuring_artists,
            "featuring_artists": featuring_artists
        }
        fill_tags = {
            "idx", "index", "track_num", "track_idx", "track_index",
            "disc_num", "disc_idx", "disc_index"
        }
        prefix_tags = {"feat_artists", "featuring_artists"}
        paren_tags = {"track_name", "track"}
        for tag in tags.keys():
            audio_file = audio_file.replace("{" + tag + "}", tags[tag])
            if tag in fill_tags:
                match = re.search(r"\{" + tag + r":\d+\}", audio_file)
                if match:
                    tokens = audio_file[match.start():match.end()]\
                        .strip("{}").split(":")
                    tag_filled = tags[tag].zfill(int(tokens[1]))
                    audio_file = audio_file[:match.start()] + tag_filled + \
                        audio_file[match.end():]
            if tag in prefix_tags:
                # don't print prefix if there are no values
                if len(tags[tag]) > 0:
                    match = re.search(r"\{" + tag + r":[^\}]+\}", audio_file)
                    if match:
                        tokens = audio_file[match.start():match.end()]\
                            .strip("{}").split(":")
                        audio_file = audio_file[:match.start()] + tokens[1] + \
                            " " + tags[tag] + audio_file[match.end():]
                else:
                    match = re.search(r"\s*\{" + tag + r":[^\}]+\}",
                                      audio_file)
                    if match:
                        audio_file = audio_file[:match.start()] + \
                                     audio_file[match.end():]
            if tag in paren_tags:
                match = re.search(r"\{" + tag + r":paren\}", audio_file)
                if match:
                    match_tag = re.search(r"(.*)\s+-\s+([^-]+)", tags[tag])
                    if match_tag:
                        audio_file = audio_file[:match.start()] + \
                                     match_tag.group(1) + " (" + \
                                     match_tag.group(2) + ")" + \
                                     audio_file[match.end():]
                    else:
                        audio_file = audio_file[:match.start()] + tags[tag] + \
                                     audio_file[match.end():]

        # in case the file name is too long
        def truncate(_str, max_size):
            return _str[:max_size].strip() if len(_str) > max_size else _str

        def truncate_dir_path(dir_path):
            path_tokens = dir_path.split(os.pathsep)
            path_tokens = [truncate(token, 255) for token in path_tokens]
            return os.pathsep.join(path_tokens)

        def truncate_file_name(file_name):
            tokens = file_name.rsplit(os.extsep, 1)
            if len(tokens) > 1:
                tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1)
            else:
                tokens[0] = truncate(tokens[0], 255)
            return os.extsep.join(tokens)

        # ensure each component in path is no more than 255 chars long
        tokens = audio_file.rsplit(os.pathsep, 1)
        if len(tokens) > 1:
            audio_file = os.path.join(truncate_dir_path(tokens[0]),
                                      truncate_file_name(tokens[1]))
        else:
            audio_file = truncate_file_name(tokens[0])

        # prepend base_dir
        audio_file = to_ascii(args, os.path.join(_base_dir, audio_file))

        # create directory if it doesn't exist
        audio_path = os.path.dirname(audio_file)
        if not os.path.exists(audio_path):
            os.makedirs(audio_path)

        return audio_file

    def prepare_rip(self, idx, track):
        args = self.args

        # reset progress
        self.progress.prepare_track(track)

        if self.progress.total_tracks > 1:
            print(Fore.GREEN + "[ " + str(idx + 1) + " / " +
                  str(self.progress.total_tracks) + " ] Ripping " +
                  track.link.uri + Fore.RESET)
        else:
            print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET)
        print(Fore.CYAN + self.audio_file + Fore.RESET)

        file_size = calc_file_size(self.args, track)
        print("Track Download Size: " + format_size(file_size))

        if args.output_type == "wav":
            self.wav_file = wave.open(self.audio_file, "wb")
            self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed'))
        elif args.output_type == "pcm":
            self.pcm_file = open(self.audio_file, 'wb')
        elif args.output_type == "flac":
            self.rip_proc = Popen([
                "flac", "-f",
                str("-" + args.comp), "--silent", "--endian", "little",
                "--channels", "2", "--bps", "16", "--sample-rate", "44100",
                "--sign", "signed", "-o", self.audio_file, "-"
            ],
                                  stdin=PIPE)
        elif args.output_type == "alac.m4a":
            self.rip_proc = Popen([
                "avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar",
                "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-",
                "-acodec", "alac", self.audio_file
            ],
                                  stdin=PIPE)
        elif args.output_type == "ogg":
            if args.cbr:
                self.rip_proc = Popen([
                    "oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o",
                    self.audio_file, "-"
                ],
                                      stdin=PIPE)
            else:
                self.rip_proc = Popen([
                    "oggenc", "--quiet", "--raw", "-q", args.vbr, "-o",
                    self.audio_file, "-"
                ],
                                      stdin=PIPE)
        elif args.output_type == "opus":
            if args.cbr:
                self.rip_proc = Popen([
                    "opusenc", "--quiet", "--comp", args.comp, "--cvbr",
                    "--bitrate",
                    str(int(args.bitrate) / 2), "--raw", "--raw-rate", "44100",
                    "-", self.audio_file
                ],
                                      stdin=PIPE)
            else:
                self.rip_proc = Popen([
                    "opusenc", "--quiet", "--comp", args.comp, "--vbr",
                    "--bitrate", args.vbr, "--raw", "--raw-rate", "44100", "-",
                    self.audio_file
                ],
                                      stdin=PIPE)
        elif args.output_type == "aac":
            if self.dev_null is None:
                self.dev_null = open(os.devnull, 'wb')
            if args.cbr:
                self.rip_proc = Popen([
                    "faac", "-P", "-X", "-b", args.bitrate, "-o",
                    self.audio_file, "-"
                ],
                                      stdin=PIPE,
                                      stdout=self.dev_null,
                                      stderr=self.dev_null)
            else:
                self.rip_proc = Popen([
                    "faac", "-P", "-X", "-q", args.vbr, "-o", self.audio_file,
                    "-"
                ],
                                      stdin=PIPE,
                                      stdout=self.dev_null,
                                      stderr=self.dev_null)
        elif args.output_type == "m4a":
            if args.cbr:
                self.rip_proc = Popen([
                    "fdkaac", "-S", "-R", "-b", args.bitrate, "-o",
                    self.audio_file, "-"
                ],
                                      stdin=PIPE)
            else:
                self.rip_proc = Popen([
                    "fdkaac", "-S", "-R", "-m", args.vbr, "-o",
                    self.audio_file, "-"
                ],
                                      stdin=PIPE)
        elif args.output_type == "mp3":
            lame_args = ["lame", "--silent"]

            if args.stereo_mode is not None:
                lame_args.extend(["-m", args.stereo_mode])

            if args.cbr:
                lame_args.extend(["-cbr", "-b", args.bitrate])
            else:
                lame_args.extend(["-V", args.vbr])

            lame_args.extend(["-h", "-r", "-", self.audio_file])
            self.rip_proc = Popen(lame_args, stdin=PIPE)

        if self.rip_proc is not None:
            self.pipe = self.rip_proc.stdin

        self.ripping.set()

    def finish_rip(self, track):
        self.progress.end_track()
        if self.pipe is not None:
            print(Fore.GREEN + 'Rip complete' + Fore.RESET)
            self.pipe.flush()
            self.pipe.close()

            # wait for process to end before continuing
            ret_code = self.rip_proc.wait()
            if ret_code != 0:
                print(Fore.YELLOW + "Warning: encoder returned non-zero "
                      "error code " + str(ret_code) + Fore.RESET)
            self.rip_proc = None
            self.pipe = None

        if self.wav_file is not None:
            self.wav_file.close()
            self.wav_file = None

        if self.pcm_file is not None:
            self.pcm_file.flush()
            os.fsync(self.pcm_file.fileno())
            self.pcm_file.close()
            self.pcm_file = None

        self.ripping.clear()
        self.post.log_success(track)

    def rip(self, session, sample_rate, frame_bytes, num_frames):
        if self.ripping.is_set():
            self.progress.update_progress(num_frames, sample_rate)
            if self.pipe is not None:
                self.pipe.write(frame_bytes)

            if self.wav_file is not None:
                self.wav_file.writeframes(frame_bytes)

            if self.pcm_file is not None:
                self.pcm_file.write(frame_bytes)

    def abort_rip(self):
        self.ripping.clear()
        self.abort.set()
Exemple #5
0
class Ripper(threading.Thread):
    name = 'SpotifyRipperThread'

    audio_file = None
    pcm_file = None
    wav_file = None
    rip_proc = None
    pipe = None
    current_playlist = None
    current_album = None
    current_chart = None

    login_success = False
    progress = None
    sync = None
    post = None
    web = None
    dev_null = None
    stop_time = None
    track_path_cache = {}
    playlist_uri = None
    rip_queue = queue.Queue()

    # threading events
    logged_in = threading.Event()
    logged_out = threading.Event()
    ripper_continue = threading.Event()
    ripping = threading.Event()
    end_of_track = threading.Event()
    finished = threading.Event()
    abort = threading.Event()
    skip = threading.Event()
    play_token_resume = threading.Event()

    def __init__(self, args):
        threading.Thread.__init__(self)

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args

        # initially logged-out
        self.logged_out.set()

        config = spotify.Config()
        default_dir = default_settings_dir()

        self.post = PostActions(args, self)
        self.web = WebAPI(args, self)

        proxy = os.environ.get('http_proxy')
        if proxy is not None:
            config.proxy = proxy

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key)
        else:
            if not path_exists(default_dir):
                os.makedirs(enc_str(default_dir))

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not path_exists(app_key_path):
                print("\n" + Fore.YELLOW +
                      "Please copy your spotify_appkey.key to " + default_dir +
                      ", or use the --key|-k option" + Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings)
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        # disable scrobbling
        self.session.social.set_scrobbling(
            spotify.SocialProvider.SPOTIFY,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.FACEBOOK,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.LASTFM,
            spotify.ScrobblingState.LOCAL_DISABLED)

        bit_rates = dict([('160', BitRate.BITRATE_160K),
                          ('320', BitRate.BITRATE_320K),
                          ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
                        self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
                        self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
                        self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
                        self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in)

        self.event_loop = EventLoop(self.session, 0.1, self)

    def stop_event_loop(self):
        if self.event_loop.isAlive():
            self.event_loop.stop()
            self.event_loop.join()

    # executes on main thread (not SpotifyRipper thread)
    def login(self):
        args = self.args

        print("Logging in...")
        if args.last:
            self.login_as_last()

        if not self.login_success and args.user is not None:
            # remove old saved password
            self.session.forget_me()

            if args.password is None:
                password = getpass.getpass()
                self.login_as_user(args.user, password)
            else:
                self.login_as_user(args.user, args.password)

        return self.login_success

    def run(self):
        args = self.args

        # start event loop
        self.event_loop.start()

        # wait for main thread to login
        self.ripper_continue.wait()
        if self.abort.is_set():
            return
        #set session to provate
        self.session.social.private_session = True

        # list of spotify URIs
        uris = args.uri

        def get_tracks_from_uri(uri):
            self.current_playlist = None
            self.current_album = None
            self.current_chart = None

            if isinstance(uri, list):
                return uri
            else:
                if (uri.startswith("spotify:artist:")
                        and (args.artist_album_type is not None
                             or args.artist_album_market is not None)):
                    album_uris = self.web.get_albums_with_filter(uri)
                    return itertools.chain(*[
                        self.load_link(album_uri) for album_uri in album_uris
                    ])
                elif uri.startswith("spotify:charts:"):
                    charts = self.web.get_charts(uri)
                    if charts is not None:
                        self.current_chart = charts
                        chart_uris = charts["tracks"]
                        return itertools.chain(*[
                            self.load_link(chart_uri)
                            for chart_uri in chart_uris
                        ])
                    else:
                        return iter([])
                else:
                    return self.load_link(uri)

        # calculate total size and time
        all_tracks = []
        for uri in uris:
            tracks = list(get_tracks_from_uri(uri))

            # TODO: remove dependency on current_album, ...
            for idx, track in enumerate(tracks):

                # ignore local tracks
                if track.is_local:
                    continue

                audio_file = self.format_track_path(idx, track)
                all_tracks.append((track, audio_file))

        self.progress.calc_total(all_tracks)

        if self.progress.total_size > 0:
            print("Total Download Size: " +
                  format_size(self.progress.total_size))

        # create track iterator
        for uri in uris:
            if self.abort.is_set():
                break

            tracks = list(get_tracks_from_uri(uri))

            if args.playlist_sync and self.current_playlist:
                self.sync = Sync(args, self)
                self.sync.sync_playlist(self.current_playlist)

            # ripping loop
            for idx, track in enumerate(tracks):
                try:
                    self.check_stop_time()
                    self.skip.clear()

                    if self.abort.is_set():
                        break

                    print('Loading track...')
                    track.load(args.timeout)
                    if track.availability != 1 or track.is_local:
                        print(Fore.RED + 'Track is not available, '
                              'skipping...' + Fore.RESET)
                        self.post.log_failure(track)
                        continue

                    self.audio_file = self.format_track_path(idx, track)

                    if not args.overwrite and path_exists(self.audio_file):
                        if is_partial(self.audio_file, track):
                            print("Overwriting partial file")
                        else:
                            print(Fore.YELLOW + "Skipping " + track.link.uri +
                                  Fore.RESET)
                            print(Fore.CYAN + self.audio_file + Fore.RESET)
                            continue

                    self.session.player.load(track)
                    self.prepare_rip(idx, track)
                    self.session.player.play()

                    timeout_count = 0
                    while not self.end_of_track.is_set() or \
                            not self.rip_queue.empty():
                        try:
                            if self.abort.is_set() or self.skip.is_set():
                                break

                            rip_item = self.rip_queue.get(timeout=1)

                            if self.abort.is_set() or self.skip.is_set():
                                break

                            self.rip(self.session, rip_item[0], rip_item[1],
                                     rip_item[2])
                        except queue.Empty:
                            timeout_count += 1
                            if timeout_count > 60:
                                raise spotify.Error("Timeout while "
                                                    "ripping track")

                    if self.skip.is_set():
                        extra_line = "" if self.play_token_resume.is_set() \
                                        else "\n"
                        print(extra_line + Fore.YELLOW +
                              "User skipped track... " + Fore.RESET)
                        self.session.player.play(False)
                        self.post.clean_up_partial()
                        self.post.log_failure(track)
                        self.end_of_track.clear()
                        self.progress.end_track(show_end=False)
                        self.ripping.clear()
                        continue

                    if self.abort.is_set():
                        self.session.player.play(False)
                        self.end_of_track.set()
                        self.post.clean_up_partial()
                        self.post.log_failure(track)
                        break

                    self.end_of_track.clear()

                    self.finish_rip(track)

                    # update id3v2 with metadata and embed front cover image
                    set_metadata_tags(args, self.audio_file, idx, track, self)

                    # finally log success
                    self.post.log_success(track)

                except (spotify.Error, Exception) as e:
                    if isinstance(e, Exception):
                        print(Fore.RED + "Spotify error detected" + Fore.RESET)
                    print(str(e))
                    traceback.print_exc()
                    print("Skipping to next track...")
                    self.session.player.play(False)
                    self.post.clean_up_partial()
                    self.post.log_failure(track)
                    continue

            # create playlist m3u file if needed
            self.post.create_playlist_m3u(tracks)

            # create playlist wpl file if needed
            self.post.create_playlist_wpl(tracks)

            # remove libspotify's offline storage cache
            self.post.remove_offline_cache()

        # logout, we are done
        self.post.end_failure_log()
        self.post.print_summary()
        self.logout()
        self.stop_event_loop()
        self.finished.set()
        sys.exit()

    def check_stop_time(self):
        args = self.args

        def wait_for_resume(resume_time):
            while datetime.now() < resume_time and not self.abort.is_set():
                time.sleep(1)

        def stop_time_triggered():
            print(Fore.YELLOW + "Stop time of " +
                  self.stop_time.strftime("%H:%M") +
                  " has been triggered, stopping..." + Fore.RESET)

            if args.resume_after is not None:
                resume_time = parse_time_str(args.resume_after)
                print(Fore.YELLOW + "Script will resume at " +
                      resume_time.strftime("%H:%M") + Fore.RESET)
                wait_for_resume(resume_time)
                self.stop_time = None
            else:
                self.abort.set()

        if args.stop_after is not None:
            if self.stop_time is None:
                self.stop_time = parse_time_str(args.stop_after)
                print(Fore.YELLOW + "Script will stop after " +
                      self.stop_time.strftime("%H:%M") + Fore.RESET)

            if self.stop_time < datetime.now():
                stop_time_triggered()

        # we also wait if the "play token" was lost
        elif self.play_token_resume.is_set():
            resume_time = parse_time_str(args.play_token_resume)
            print(Fore.YELLOW + "Script will resume at " +
                  resume_time.strftime("%H:%M") + Fore.RESET)
            wait_for_resume(resume_time)
            self.play_token_resume.clear()

    def load_link(self, uri):
        # ignore if the uri is just blank (e.g. from a file)
        if not uri:
            return iter([])
        trackList = []
        uriList = []
        args = self.args
        link = self.session.get_link(uri)
        track_list = []
        if link.type == spotify.LinkType.TRACK:
            track = link.as_track()
            return iter([track])
        elif link.type == spotify.LinkType.PLAYLIST:
            print('get playlist tracks')
            self.playlist_uri = uri
            tracks = get_playlist_tracks(self.session.user.canonical_name, uri)
            track_list = tracks.get('items')
            for n in track_list:
                thisTrack = n.get('track')
                thisTrackuri = thisTrack.get('uri')
                uriList.append(thisTrackuri)
            tracksIter = iter(uriList)
            for i in tracksIter:
                trackList.append(self.session.get_link(i).as_track())
            print('Loading playlist...')
            return iter(trackList)
        elif link.type == spotify.LinkType.STARRED:
            link_user = link.as_user()

            def load_starred():
                if link_user is not None:
                    return self.session.get_starred(link_user.canonical_name)
                else:
                    return self.session.get_starred()

            starred = load_starred()
            attempt_count = 1
            while starred is None:
                if attempt_count > 3:
                    print(Fore.RED + "Could not load starred playlist..." +
                          Fore.RESET)
                    return iter([])
                print("Attempt " + str(attempt_count) + " failed: Spotify " +
                      "returned None for starred playlist, trying again in " +
                      "5 seconds...")
                time.sleep(5.0)
                starred = load_starred()
                attempt_count += 1
            print('Loading starred playlist...')
            starred.load(args.timeout)
            return iter(starred.tracks)
        elif link.type == spotify.LinkType.ALBUM:
            album = link.as_album()
            album_browser = album.browse()
            print('Loading album browser...')
            album_browser.load(args.timeout)
            self.current_album = album
            return iter(album_browser.tracks)
        elif link.type == spotify.LinkType.ARTIST:
            artist = link.as_artist()
            artist_browser = artist.browse()
            print('Loading artist browser...')
            artist_browser.load(args.timeout)
            return iter(artist_browser.tracks)
        return iter([])

    def search_query(self, query):
        print("Searching for query: " + query)

        try:
            result = self.session.search(query)
            result.load(self.args.timeout)
        except spotify.Error as e:
            print(str(e))
            return iter([])

        if len(result.tracks) == 0:
            print(Fore.RED + "No Results" + Fore.RESET)
            return iter([])

        # list tracks
        print(Fore.GREEN + "Results" + Fore.RESET)
        for track_idx, track in enumerate(result.tracks):
            print("  " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET + " [" +
                  to_ascii(track.album.name) + "] " +
                  to_ascii(track.artists[0].name) + " - " +
                  to_ascii(track.name) + " (" + str(track.popularity) + ")")

        pick = raw_input("Pick track(s) (ex 1-3,5): ")

        def get_track(i):
            if i >= 0 and i < len(result.tracks):
                return iter([result.tracks[i]])
            return iter([])

        pattern = re.compile("^[0-9 ,\-]+$")
        if pick.isdigit():
            pick = int(pick) - 1
            return get_track(pick)
        elif pick.lower() == "a" or pick.lower() == "all":
            return iter(result.tracks)
        elif pattern.match(pick):

            def range_string(comma_string):
                def hyphen_range(hyphen_string):
                    x = [int(x) - 1 for x in hyphen_string.split('-')]
                    return range(x[0], x[-1] + 1)

                return itertools.chain(
                    *[hyphen_range(r) for r in comma_string.split(',')])

            picks = sorted(set(list(range_string(pick))))
            return itertools.chain(*[get_track(p) for p in picks])

        if pick != "":
            print(Fore.RED + "Invalid selection" + Fore.RESET)
        return iter([])

    def on_music_delivery(self, session, audio_format, frame_bytes,
                          num_frames):
        try:
            self.rip_queue.put_nowait(
                (audio_format.sample_rate, frame_bytes, num_frames))
        except queue.Full:
            print(Fore.RED + "rip_queue is full. dropped music data" +
                  Fore.RESET)
        return num_frames

    def on_connection_state_changed(self, session):
        if session.connection.state is spotify.ConnectionState.LOGGED_IN:
            self.login_success = True
            self.logged_in.set()
            self.logged_out.clear()
        elif session.connection.state is spotify.ConnectionState.LOGGED_OUT:
            self.logged_in.clear()
            self.ripper_continue.clear()
            self.logged_out.set()

    def on_logged_in(self, session, error):
        if error is spotify.ErrorType.OK:
            print("Logged in as " + session.user.display_name)
        else:
            error_map = {
                9: "CLIENT_TOO_OLD",
                8: "UNABLE_TO_CONTACT_SERVER",
                6: "BAD_USERNAME_OR_PASSWORD",
                7: "USER_BANNED",
                15: "USER_NEEDS_PREMIUM",
                16: "OTHER_TRANSIENT",
                10: "OTHER_PERMANENT"
            }
            print("Logged in failed: " +
                  error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error)))
            self.login_success = False
            self.logged_in.set()

    def play_token_lost(self, session):
        if self.args.play_token_resume is not None:
            print("\n" + Fore.RED + "Play token lost, waiting " +
                  self.args.play_token_resume + " to resume..." + Fore.RESET)
            self.play_token_resume.set()
            self.skip.set()
        else:
            print("\n" + Fore.RED + "Play token lost, aborting..." +
                  Fore.RESET)
            self.abort_rip()

    def on_end_of_track(self, session):
        self.session.player.play(False)
        self.end_of_track.set()

    def login_as_user(self, user, password):
        """login into Spotify"""
        self.session.login(user, password, remember_me=True)
        self.logged_in.wait()

    def login_as_last(self):
        """login as the previous logged in user"""
        try:
            self.session.relogin()
            self.logged_in.wait()
        except spotify.Error as e:
            self.login_success = False
            print(str(e))

    def logout(self):
        """logout from Spotify"""
        time.sleep(0.1)
        if self.logged_in.is_set():
            print('Logging out...')
            self.session.logout()
            self.logged_out.wait()

    def format_track_path(self, idx, track):
        args = self.args

        # check if we cached the result already
        track.load(args.timeout)
        if track.link.uri in self.track_path_cache:
            return self.track_path_cache[track.link.uri]

        audio_file = \
            format_track_string(self, args.format.strip(), idx, track)

        # in case the file name is too long
        def truncate(_str, max_size):
            return _str[:max_size].strip() if len(_str) > max_size else _str

        def truncate_dir_path(dir_path):
            path_tokens = dir_path.split(os.sep)
            path_tokens = [truncate(token, 255) for token in path_tokens]
            return os.sep.join(path_tokens)

        def truncate_file_name(file_name):
            tokens = file_name.rsplit(os.extsep, 1)
            if len(tokens) > 1:
                tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1)
            else:
                tokens[0] = truncate(tokens[0], 255)
            return os.extsep.join(tokens)

        # ensure each component in path is no more than 255 chars long
        if args.windows_safe:
            tokens = audio_file.rsplit(os.sep, 1)
            if len(tokens) > 1:
                audio_file = os.path.join(truncate_dir_path(tokens[0]),
                                          truncate_file_name(tokens[1]))
            else:
                audio_file = truncate_file_name(tokens[0])

        # replace filename
        if args.replace is not None:
            audio_file = self.replace_filename(audio_file, args.replace)

        # remove not allowed characters in filename (windows)
        if args.windows_safe:
            audio_file = re.sub('[:"*?<>|]', '', audio_file)

        # prepend base_dir
        audio_file = to_ascii(os.path.join(base_dir(), audio_file))

        if args.normalized_ascii:
            audio_file = to_normalized_ascii(audio_file)

        # create directory if it doesn't exist
        audio_path = os.path.dirname(audio_file)
        if not path_exists(audio_path):
            os.makedirs(enc_str(audio_path))

        self.track_path_cache[track.link.uri] = audio_file
        return audio_file

    def replace_filename(self, filename, pattern_list):
        for pattern in pattern_list:
            repl = pattern.split('/')
            filename = re.sub(repl[0], repl[1], filename)
        return filename

    def prepare_rip(self, idx, track):
        args = self.args

        # reset progress
        self.progress.prepare_track(track)

        if self.progress.total_tracks > 1:
            print(Fore.GREEN + "[ " + str(self.progress.track_idx) + " / " +
                  str(self.progress.total_tracks +
                      self.progress.skipped_tracks) + " ] Ripping " +
                  track.link.uri + Fore.WHITE + "\t(ESC to skip)" + Fore.RESET)
        else:
            print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET)
        print(Fore.CYAN + self.audio_file + Fore.RESET)

        file_size = calc_file_size(track)
        print("Track Download Size: " + format_size(file_size))

        if args.output_type == "wav" or args.plus_wav:
            audio_file = change_file_extension(self.audio_file, "wav") if \
                args.output_type != "wav" else self.audio_file
            wav_file = audio_file if sys.version_info >= (3, 0) \
                else enc_str(audio_file)
            self.wav_file = wave.open(wav_file, "wb")
            self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed'))

        if args.output_type == "pcm" or args.plus_pcm:
            audio_file = change_file_extension(self.audio_file, "pcm") if \
                args.output_type != "pcm" else self.audio_file
            self.pcm_file = open(enc_str(audio_file), 'wb')

        audio_file_enc = enc_str(self.audio_file)

        if args.output_type == "flac":
            self.rip_proc = Popen([
                "flac", "-f", ("-" + str(args.comp)), "--silent", "--endian",
                "little", "--channels", "2", "--bps", "16", "--sample-rate",
                "44100", "--sign", "signed", "-o", audio_file_enc, "-"
            ],
                                  stdin=PIPE)
        elif args.output_type == "aiff":
            self.rip_proc = Popen([
                "sox", "-q", "--endian", "little", "--channels", "2", "--bits",
                "16", "--rate", "44100", "--encoding", "unsigned-integer",
                "-t", "raw", "-", audio_file_enc
            ],
                                  stdin=PIPE)
        elif args.output_type == "alac.m4a":
            self.rip_proc = Popen([
                "avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar",
                "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-",
                "-acodec", "alac", audio_file_enc
            ],
                                  stdin=PIPE)
        elif args.output_type == "ogg":
            if args.cbr:
                self.rip_proc = Popen([
                    "oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o",
                    audio_file_enc, "-"
                ],
                                      stdin=PIPE)
            else:
                self.rip_proc = Popen([
                    "oggenc", "--quiet", "--raw", "-q", args.vbr, "-o",
                    audio_file_enc, "-"
                ],
                                      stdin=PIPE)
        elif args.output_type == "opus":
            if args.cbr:
                self.rip_proc = Popen([
                    "opusenc", "--quiet", "--comp", args.comp, "--cvbr",
                    "--bitrate",
                    str(int(args.bitrate) / 2), "--raw", "--raw-rate", "44100",
                    "-", audio_file_enc
                ],
                                      stdin=PIPE)
            else:
                self.rip_proc = Popen([
                    "opusenc", "--quiet", "--comp", args.comp, "--vbr",
                    "--bitrate", args.vbr, "--raw", "--raw-rate", "44100", "-",
                    audio_file_enc
                ],
                                      stdin=PIPE)
        elif args.output_type == "aac":
            if self.dev_null is None:
                self.dev_null = open(os.devnull, 'wb')
            if args.cbr:
                self.rip_proc = Popen([
                    "faac", "-P", "-X", "-b", args.bitrate, "-o",
                    audio_file_enc, "-"
                ],
                                      stdin=PIPE,
                                      stdout=self.dev_null,
                                      stderr=self.dev_null)
            else:
                self.rip_proc = Popen([
                    "faac", "-P", "-X", "-q", args.vbr, "-o", audio_file_enc,
                    "-"
                ],
                                      stdin=PIPE,
                                      stdout=self.dev_null,
                                      stderr=self.dev_null)
        elif args.output_type == "m4a":
            if args.cbr:
                self.rip_proc = Popen([
                    "fdkaac", "-S", "-R", "-b", args.bitrate, "-o",
                    audio_file_enc, "-"
                ],
                                      stdin=PIPE)
            else:
                self.rip_proc = Popen([
                    "fdkaac", "-S", "-R", "-m", args.vbr, "-o", audio_file_enc,
                    "-"
                ],
                                      stdin=PIPE)
        elif args.output_type == "mp3":
            lame_args = ["lame", "--silent"]

            if args.stereo_mode is not None:
                lame_args.extend(["-m", args.stereo_mode])

            if args.cbr:
                lame_args.extend(["-cbr", "-b", args.bitrate])
            else:
                lame_args.extend(["-V", args.vbr])

            lame_args.extend(["-h", "-r", "-", audio_file_enc])
            self.rip_proc = Popen(lame_args, stdin=PIPE)

        if self.rip_proc is not None:
            self.pipe = self.rip_proc.stdin

        self.ripping.set()

    def finish_rip(self, track):
        self.progress.end_track()
        if self.pipe is not None:
            print(Fore.GREEN + 'Rip complete' + Fore.RESET)
            self.pipe.flush()
            self.pipe.close()

            # wait for process to end before continuing
            ret_code = self.rip_proc.wait()
            if ret_code != 0:
                print(Fore.YELLOW + "Warning: encoder returned non-zero "
                      "error code " + str(ret_code) + Fore.RESET)
            self.rip_proc = None
            self.pipe = None

        if self.wav_file is not None:
            self.wav_file.close()
            self.wav_file = None

        if self.pcm_file is not None:
            self.pcm_file.flush()
            os.fsync(self.pcm_file.fileno())
            self.pcm_file.close()
            self.pcm_file = None

        self.ripping.clear()

    def rip(self, session, sample_rate, frame_bytes, num_frames):
        if self.ripping.is_set():
            self.progress.update_progress(num_frames, sample_rate)
            if self.pipe is not None:
                self.pipe.write(frame_bytes)

            if self.wav_file is not None:
                self.wav_file.writeframes(frame_bytes)

            if self.pcm_file is not None:
                self.pcm_file.write(frame_bytes)

    def abort_rip(self):
        self.ripping.clear()
        self.abort.set()
Exemple #6
0
    def __init__(self, args):
        threading.Thread.__init__(self)

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args

        # initially logged-out
        self.logged_out.set()

        config = spotify.Config()
        default_dir = default_settings_dir()

        self.post = PostActions(args, self)

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print("\n" + Fore.YELLOW +
                      "Please copy your spotify_appkey.key to " +
                      default_dir + ", or use the --key|-k option" +
                      Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        # disable scrobbling
        self.session.social.set_scrobbling(
            spotify.SocialProvider.SPOTIFY,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.FACEBOOK,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.LASTFM,
            spotify.ScrobblingState.LOCAL_DISABLED)

        bit_rates = dict([
            ('160', BitRate.BITRATE_160K),
            ('320', BitRate.BITRATE_320K),
            ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
                        self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
                        self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
                        self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
                        self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN,
                        self.on_logged_in)

        self.event_loop = EventLoop(self.session, 0.1, self)
Exemple #7
0
class Ripper(threading.Thread):
    name = 'SpotifyRipperThread'

    audio_file = None
    pcm_file = None
    wav_file = None
    rip_proc = None
    pipe = None
    current_playlist = None
    current_album = None

    idx_digits = 3
    login_success = False
    progress = None
    sync = None
    post = None
    dev_null = None

    rip_queue = queue.Queue()

    # threading events
    logged_in = threading.Event()
    logged_out = threading.Event()
    ripping = threading.Event()
    end_of_track = threading.Event()
    finished = threading.Event()
    abort = threading.Event()

    def __init__(self, args):
        threading.Thread.__init__(self)

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args

        # initially logged-out
        self.logged_out.set()

        config = spotify.Config()
        default_dir = default_settings_dir()

        self.post = PostActions(args, self)

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print("\n" + Fore.YELLOW +
                      "Please copy your spotify_appkey.key to " +
                      default_dir + ", or use the --key|-k option" +
                      Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        # disable scrobbling
        self.session.social.set_scrobbling(
            spotify.SocialProvider.SPOTIFY,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.FACEBOOK,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.LASTFM,
            spotify.ScrobblingState.LOCAL_DISABLED)

        bit_rates = dict([
            ('160', BitRate.BITRATE_160K),
            ('320', BitRate.BITRATE_320K),
            ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
                        self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
                        self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
                        self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
                        self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN,
                        self.on_logged_in)

        self.event_loop = EventLoop(self.session, 0.1, self)

    def stop_event_loop(self):
        if self.event_loop.isAlive():
            self.event_loop.stop()
            self.event_loop.join()

    def run(self):
        args = self.args

        # start event loop
        self.event_loop.start()

        # login
        print("Logging in...")
        if args.last:
            self.login_as_last()
        elif args.user is not None and args.password is None:
            password = getpass.getpass()
            self.login(args.user[0], password)
        else:
            self.login(args.user[0], args.password[0])

        if not self.login_success:
            print(
                Fore.RED + "Encountered issue while logging into "
                           "Spotify, aborting..." + Fore.RESET)
            self.stop_event_loop()
            self.finished.set()
            return

        # check if we were passed a file name or search
        if len(args.uri) == 1 and os.path.exists(args.uri[0]):
            uris = [line.strip() for line in open(args.uri[0])]
        elif len(args.uri) == 1 and not args.uri[0].startswith("spotify:"):
            uris = [list(self.search_query(args.uri[0]))]
        else:
            uris = args.uri

        def get_tracks_from_uri(uri):
            if isinstance(uri, list):
                return uri
            else:
                if (args.exclude_appears_on and
                        uri.startswith("spotify:artist:")):
                    album_uris = self.load_artist_albums(uri)
                    return itertools.chain(
                        *[self.load_link(album_uri) for
                          album_uri in album_uris])
                else:
                    return self.load_link(uri)

        # calculate total size and time
        all_tracks = []
        for uri in uris:
            tracks = get_tracks_from_uri(uri)
            all_tracks += list(tracks)

        self.progress.calc_total(all_tracks)

        if self.progress.total_size > 0:
            print(
                "Total Download Size: " +
                format_size(self.progress.total_size))

        # create track iterator
        for uri in uris:
            if self.abort.is_set():
                break

            tracks = list(get_tracks_from_uri(uri))

            if args.flat_with_index and self.current_playlist:
                self.idx_digits = len(str(len(self.current_playlist.tracks)))

            if args.playlist_sync and self.current_playlist:
                self.sync = Sync(args, self)
                self.sync.sync_playlist(self.current_playlist)

            # ripping loop
            for idx, track in enumerate(tracks):
                try:
                    if self.abort.is_set():
                        break

                    print('Loading track...')
                    track.load()
                    if track.availability != 1:
                        print(
                            Fore.RED + 'Track is not available, '
                                       'skipping...' + Fore.RESET)
                        self.post.log_failure(track)
                        continue

                    self.audio_file = self.format_track_path(idx, track)

                    if not args.overwrite and os.path.exists(self.audio_file):
                        print(
                            Fore.YELLOW + "Skipping " +
                            track.link.uri + Fore.RESET)
                        print(Fore.CYAN + self.audio_file + Fore.RESET)
                        self.post.queue_remove_from_playlist(idx)
                        continue

                    self.session.player.load(track)
                    self.prepare_rip(idx, track)
                    self.session.player.play()

                    while not self.end_of_track.is_set() or \
                            not self.rip_queue.empty():
                        try:
                            if self.abort.is_set():
                                break

                            rip_item = self.rip_queue.get(timeout=1)
                            self.rip(self.session, rip_item[0],
                                     rip_item[1], rip_item[2])
                        except queue.Empty:
                            pass

                    if self.abort.is_set():
                        self.session.player.play(False)
                        self.end_of_track.set()
                        self.post.clean_up_partial()
                        self.post.log_failure(track)
                        break

                    self.end_of_track.clear()

                    self.finish_rip(track)

                    # update id3v2 with metadata and embed front cover image
                    set_metadata_tags(args, self.audio_file, track)

                    # make a note of the index and remove all the
                    # tracks from the playlist when everything is done
                    self.post.queue_remove_from_playlist(idx)

                except (spotify.Error, Exception) as e:
                    if isinstance(e, Exception):
                        print(Fore.RED + "Spotify error detected" + Fore.RESET)
                    print(str(e))
                    print("Skipping to next track...")
                    self.session.player.play(False)
                    self.post.clean_up_partial()
                    self.post.log_failure(track)
                    continue

            if not self.abort.is_set():
                # create playlist m3u file if needed
                self.post.create_playlist_m3u(tracks)

                # create playlist wpl file if needed
                self.post.create_playlist_wpl(tracks)

                # actually removing the tracks from playlist
                self.post.remove_tracks_from_playlist()

        # logout, we are done
        self.post.end_failure_log()
        self.post.print_summary()
        self.logout()
        self.stop_event_loop()
        self.finished.set()

    def load_link(self, uri):
        # blank out current playlist/album
        self.current_playlist = None
        self.current_album = None

        # ignore if the uri is just blank (e.g. from a file)
        if not uri:
            return iter([])

        link = self.session.get_link(uri)
        if link.type == spotify.LinkType.TRACK:
            track = link.as_track()
            return iter([track])
        elif link.type == spotify.LinkType.PLAYLIST:
            self.current_playlist = link.as_playlist()
            print('Loading playlist...')
            self.current_playlist.load()
            return iter(self.current_playlist.tracks)
        elif link.type == spotify.LinkType.STARRED:
            link_user = link.as_user()
            if link_user is not None:
                starred = self.session.get_starred(link_user.canonical_name)
            else:
                starred = self.session.get_starred()

            if starred is not None:
                print('Loading starred playlist...')
                starred.load()
                return iter(starred.tracks)
            else:
                print(
                    Fore.RED + "Could not load starred playlist..." +
                    Fore.RESET)
                return iter([])
        elif link.type == spotify.LinkType.ALBUM:
            album = link.as_album()
            album_browser = album.browse()
            print('Loading album browser...')
            album_browser.load()
            self.current_album = album
            return iter(album_browser.tracks)
        elif link.type == spotify.LinkType.ARTIST:
            artist = link.as_artist()
            artist_browser = artist.browse()
            print('Loading artist browser...')
            artist_browser.load()
            return iter(artist_browser.tracks)
        return iter([])

    # excludes 'appears on' albums
    def load_artist_albums(self, uri):
        def get_albums_json(offset):
            url = 'https://api.spotify.com/v1/artists/' + \
                  uri_tokens[2] + \
                  '/albums/?=album_type=album,single,compilation' + \
                  '&limit=50&offset=' + str(offset)
            print(
                Fore.GREEN + "Attempting to retrieve albums "
                             "from Spotify's Web API" + Fore.RESET)
            print(Fore.CYAN + url + Fore.RESET)
            req = requests.get(url)
            if req.status_code == 200:
                return req.json()
            else:
                print(Fore.YELLOW + "URL returned non-200 HTTP code: " +
                      str(req.status_code) + Fore.RESET)
            return None

        # extract artist id from uri
        uri_tokens = uri.split(':')
        if len(uri_tokens) != 3:
            return []

        # it is possible we won't get all the albums on the first request
        offset = 0
        album_uris = []
        total = None
        while total is None or offset < total:
            try:
                # rate limit if not first request
                if total is None:
                    time.sleep(1.0)
                albums = get_albums_json(offset)
                if albums is None:
                    break

                # extract album URIs
                album_uris += [album['uri'] for album in albums['items']]
                offset = len(album_uris)
                if total is None:
                    total = albums['total']
            except KeyError as e:
                break
        print(str(len(album_uris)) + " albums found")
        return album_uris

    def search_query(self, query):
        args = self.args

        print("Searching for query: " + query)
        try:
            result = self.session.search(query)
            result.load()
        except spotify.Error as e:
            print(str(e))
            return iter([])

        # list tracks
        print(Fore.GREEN + "Results" + Fore.RESET)
        for track_idx, track in enumerate(result.tracks):
            print("  " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET +
                  " [" + to_ascii(args, track.album.name) + "] " +
                  to_ascii(args, track.artists[0].name) + " - " +
                  to_ascii(args, track.name) +
                  " (" + str(track.popularity) + ")")

        pick = raw_input("Pick track(s) (ex 1-3,5): ")

        def get_track(i):
            if i >= 0 and i < len(result.tracks):
                return iter([result.tracks[i]])
            return iter([])

        pattern = re.compile("^[0-9 ,\-]+$")
        if pick.isdigit():
            pick = int(pick) - 1
            return get_track(pick)
        elif pick.lower() == "a" or pick.lower() == "all":
            return iter(result.tracks)
        elif pattern.match(pick):
            def range_string(comma_string):
                def hyphen_range(hyphen_string):
                    x = [int(x) - 1 for x in hyphen_string.split('-')]
                    return range(x[0], x[-1] + 1)

                return itertools.chain(
                    *[hyphen_range(r) for r in comma_string.split(',')])

            picks = sorted(set(list(range_string(pick))))
            return itertools.chain(*[get_track(p) for p in picks])

        if pick != "":
            print(Fore.RED + "Invalid selection" + Fore.RESET)
        return iter([])

    def on_music_delivery(self, session, audio_format,
                          frame_bytes, num_frames):
        try:
            self.rip_queue.put_nowait((audio_format.sample_rate,
                                       frame_bytes, num_frames))
        except queue.Full:
            print(Fore.RED + "rip_queue is full. dropped music data" +
                  Fore.RESET)
        return num_frames

    def on_connection_state_changed(self, session):
        if session.connection.state is spotify.ConnectionState.LOGGED_IN:
            self.login_success = True
            self.logged_in.set()
            self.logged_out.clear()
        elif session.connection.state is spotify.ConnectionState.LOGGED_OUT:
            self.logged_in.clear()
            self.logged_out.set()

    def on_logged_in(self, session, error):
        if error is spotify.ErrorType.OK:
            print("Logged in as " + session.user.display_name)
        else:
            error_map = {
                9: "CLIENT_TOO_OLD",
                8: "UNABLE_TO_CONTACT_SERVER",
                6: "BAD_USERNAME_OR_PASSWORD",
                7: "USER_BANNED",
                15: "USER_NEEDS_PREMIUM",
                16: "OTHER_TRANSIENT",
                10: "OTHER_PERMANENT"
            }
            print("Logged in failed: " +
                  error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error)))
            self.login_success = False
            self.logged_in.set()

    def play_token_lost(self, session):
        print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET)
        self.abort_rip()

    def on_end_of_track(self, session):
        self.session.player.play(False)
        self.end_of_track.set()

    def login(self, user, password):
        """login into Spotify"""
        self.session.login(user, password, remember_me=True)
        self.logged_in.wait()

    def login_as_last(self):
        """login as the previous logged in user"""
        try:
            self.session.relogin()
            self.logged_in.wait()
        except spotify.Error as e:
            self.login_success = False
            print(str(e))

    def logout(self):
        """logout from Spotify"""
        time.sleep(0.1)
        if self.logged_in.is_set():
            print('Logging out...')
            self.session.logout()
            self.logged_out.wait()

    def album_artists_web(self, uri):
        def get_album_json(album_id):
            url = 'https://api.spotify.com/v1/albums/' + album_id
            print(
                Fore.GREEN + "Attempting to retrieve album "
                             "from Spotify's Web API" + Fore.RESET)
            print(Fore.CYAN + url + Fore.RESET)
            req = requests.get(url)
            if req.status_code == 200:
                return req.json()
            else:
                print(Fore.YELLOW + "URL returned non-200 HTTP code: " +
                      str(req.status_code) + Fore.RESET)
            return None

        # extract album id from uri
        uri_tokens = uri.split(':')
        if len(uri_tokens) != 3:
            return None

        album = get_album_json(uri_tokens[2])
        if album is None:
            return None

        return [artist['name'] for artist in album['artists']]

    def format_track_path(self, idx, track):
        args = self.args
        _base_dir = base_dir(args)
        audio_file = args.format[0].strip()

        track_artist = to_ascii(
            args, escape_filename_part(track.artists[0].name))
        track_artists = to_ascii(args, ", ".join(
            [artist.name for artist in track.artists]))
        if len(track.artists) > 1:
            featuring_artists = to_ascii(args, ", ".join(
                [artist.name for artist in track.artists[1:]]))
        else:
            featuring_artists = ""

        album_artist = to_ascii(
            args,
            self.current_album.artist.name
            if self.current_album is not None else track_artist)
        album_artists_web = track_artists

        # only retrieve album_artist_web if it exists in the format string
        if (self.current_album is not None and
                audio_file.find("{album_artists_web}") >= 0):
            artist_array = self.album_artists_web(self.current_album.link.uri)
            if artist_array is not None:
                album_artists_web = to_ascii(args, ", ".join(artist_array))

        album = to_ascii(args, escape_filename_part(track.album.name))
        track_name = to_ascii(args, escape_filename_part(track.name))
        year = str(track.album.year)
        extension = args.output_type
        idx_str = str(idx + 1)
        track_num = str(track.index)
        disc_num = str(track.disc)
        if self.current_playlist is not None:
            playlist_name = to_ascii(args, self.current_playlist.name)
            playlist_owner = to_ascii(
                args, self.current_playlist.owner.display_name)
        else:
            playlist_name = "No Playlist"
            playlist_owner = "No Playlist Owner"
        user = self.session.user.display_name

        tags = {
            "track_artist": track_artist,
            "track_artists": track_artists,
            "album_artist": album_artist,
            "album_artists_web": album_artists_web,
            "artist": track_artist,
            "artists": track_artists,
            "album": album,
            "track_name": track_name,
            "track": track_name,
            "year": year,
            "ext": extension,
            "extension": extension,
            "idx": idx_str,
            "index": idx_str,
            "track_num": track_num,
            "track_idx": track_num,
            "track_index": track_num,
            "disc_num": disc_num,
            "disc_idx": disc_num,
            "disc_index": disc_num,
            "playlist": playlist_name,
            "playlist_name": playlist_name,
            "playlist_owner": playlist_owner,
            "playlist_user": playlist_owner,
            "playlist_username": playlist_owner,
            "user": user,
            "username": user,
            "feat_artists": featuring_artists,
            "featuring_artists": featuring_artists
        }
        fill_tags = {"idx", "index", "track_num", "track_idx",
                     "track_index", "disc_num", "disc_idx", "disc_index"}
        prefix_tags = {"feat_artists", "featuring_artists"}
        paren_tags = {"track_name", "track"}
        for tag in tags.keys():
            audio_file = audio_file.replace("{" + tag + "}", tags[tag])
            if tag in fill_tags:
                match = re.search(r"\{" + tag + r":\d+\}", audio_file)
                if match:
                    tokens = audio_file[match.start():match.end()]\
                        .strip("{}").split(":")
                    tag_filled = tags[tag].zfill(int(tokens[1]))
                    audio_file = audio_file[:match.start()] + tag_filled + \
                        audio_file[match.end():]
            if tag in prefix_tags:
                # don't print prefix if there are no values
                if len(tags[tag]) > 0:
                    match = re.search(r"\{" + tag + r":[^\}]+\}", audio_file)
                    if match:
                        tokens = audio_file[match.start():match.end()]\
                            .strip("{}").split(":")
                        audio_file = audio_file[:match.start()] + tokens[1] + \
                            " " + tags[tag] + audio_file[match.end():]
                else:
                    match = re.search(r"\s*\{" + tag +
                                      r":[^\}]+\}", audio_file)
                    if match:
                        audio_file = audio_file[:match.start()] + \
                                     audio_file[match.end():]
            if tag in paren_tags:
                match = re.search(r"\{" + tag + r":paren\}", audio_file)
                if match:
                    match_tag = re.search(r"(.*)\s+-\s+([^-]+)", tags[tag])
                    if match_tag:
                        audio_file = audio_file[:match.start()] + \
                                     match_tag.group(1) + " (" + \
                                     match_tag.group(2) + ")" + \
                                     audio_file[match.end():]
                    else:
                        audio_file = audio_file[:match.start()] + tags[tag] + \
                                     audio_file[match.end():]

        # in case the file name is too long
        def truncate(_str, max_size):
            return _str[:max_size].strip() if len(_str) > max_size else _str

        def truncate_dir_path(dir_path):
            path_tokens = dir_path.split(os.pathsep)
            path_tokens = [truncate(token, 255) for token in path_tokens]
            return os.pathsep.join(path_tokens)

        def truncate_file_name(file_name):
            tokens = file_name.rsplit(os.extsep, 1)
            if len(tokens) > 1:
                tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1)
            else:
                tokens[0] = truncate(tokens[0], 255)
            return os.extsep.join(tokens)

        # ensure each component in path is no more than 255 chars long
        tokens = audio_file.rsplit(os.pathsep, 1)
        if len(tokens) > 1:
            audio_file = os.path.join(
                truncate_dir_path(tokens[0]), truncate_file_name(tokens[1]))
        else:
            audio_file = truncate_file_name(tokens[0])

        # prepend base_dir
        audio_file = to_ascii(args, os.path.join(_base_dir, audio_file))

        # create directory if it doesn't exist
        audio_path = os.path.dirname(audio_file)
        if not os.path.exists(audio_path):
            os.makedirs(audio_path)

        return audio_file

    def prepare_rip(self, idx, track):
        args = self.args

        # reset progress
        self.progress.prepare_track(track)

        if self.progress.total_tracks > 1:
            print(Fore.GREEN + "[ " + str(idx + 1) + " / " + str(
                self.progress.total_tracks) + " ] Ripping " +
                  track.link.uri + Fore.RESET)
        else:
            print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET)
        print(Fore.CYAN + self.audio_file + Fore.RESET)

        file_size = calc_file_size(self.args, track)
        print("Track Download Size: " + format_size(file_size))

        if args.output_type == "wav":
            self.wav_file = wave.open(self.audio_file, "wb")
            self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed'))
        elif args.output_type == "pcm":
            self.pcm_file = open(self.audio_file, 'wb')
        elif args.output_type == "flac":
            self.rip_proc = Popen(
                ["flac", "-f", str("-" + args.comp), "--silent", "--endian",
                 "little", "--channels", "2", "--bps", "16", "--sample-rate",
                 "44100", "--sign", "signed", "-o", self.audio_file, "-"],
                stdin=PIPE)
        elif args.output_type == "alac.m4a":
            self.rip_proc = Popen(
                ["avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar",
                 "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-",
                 "-acodec", "alac", self.audio_file],
                stdin=PIPE)
        elif args.output_type == "ogg":
            if args.cbr:
                self.rip_proc = Popen(
                    ["oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o",
                     self.audio_file, "-"], stdin=PIPE)
            else:
                self.rip_proc = Popen(
                    ["oggenc", "--quiet", "--raw", "-q", args.vbr, "-o",
                     self.audio_file, "-"], stdin=PIPE)
        elif args.output_type == "opus":
            if args.cbr:
                self.rip_proc = Popen(
                    ["opusenc", "--quiet", "--comp", args.comp, "--cvbr",
                     "--bitrate", str(int(args.bitrate) / 2), "--raw",
                     "--raw-rate", "44100", "-", self.audio_file], stdin=PIPE)
            else:
                self.rip_proc = Popen(
                    ["opusenc", "--quiet", "--comp", args.comp, "--vbr",
                     "--bitrate", args.vbr, "--raw", "--raw-rate", "44100",
                     "-", self.audio_file], stdin=PIPE)
        elif args.output_type == "aac":
            if self.dev_null is None:
                self.dev_null = open(os.devnull, 'wb')
            if args.cbr:
                self.rip_proc = Popen(
                    ["faac", "-P", "-X", "-b", args.bitrate, "-o",
                     self.audio_file, "-"], stdin=PIPE,
                    stdout=self.dev_null, stderr=self.dev_null)
            else:
                self.rip_proc = Popen(
                    ["faac", "-P", "-X", "-q", args.vbr, "-o",
                     self.audio_file, "-"], stdin=PIPE,
                    stdout=self.dev_null, stderr=self.dev_null)
        elif args.output_type == "m4a":
            if args.cbr:
                self.rip_proc = Popen(
                    ["fdkaac", "-S", "-R", "-b",
                     args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE)
            else:
                self.rip_proc = Popen(
                    ["fdkaac", "-S", "-R", "-m", args.vbr,
                     "-o", self.audio_file, "-"], stdin=PIPE)
        elif args.output_type == "mp3":
            lame_args = ["lame", "--silent"]

            if args.stereo_mode is not None:
                lame_args.extend(["-m", args.stereo_mode])

            if args.cbr:
                lame_args.extend(["-cbr", "-b", args.bitrate])
            else:
                lame_args.extend(["-V", args.vbr])

            lame_args.extend(["-h", "-r", "-", self.audio_file])
            self.rip_proc = Popen(lame_args, stdin=PIPE)

        if self.rip_proc is not None:
            self.pipe = self.rip_proc.stdin

        self.ripping.set()

    def finish_rip(self, track):
        self.progress.end_track()
        if self.pipe is not None:
            print(Fore.GREEN + 'Rip complete' + Fore.RESET)
            self.pipe.flush()
            self.pipe.close()

            # wait for process to end before continuing
            ret_code = self.rip_proc.wait()
            if ret_code != 0:
                print(
                    Fore.YELLOW + "Warning: encoder returned non-zero "
                                  "error code " + str(ret_code) + Fore.RESET)
            self.rip_proc = None
            self.pipe = None

        if self.wav_file is not None:
            self.wav_file.close()
            self.wav_file = None

        if self.pcm_file is not None:
            self.pcm_file.flush()
            os.fsync(self.pcm_file.fileno())
            self.pcm_file.close()
            self.pcm_file = None

        self.ripping.clear()
        self.post.log_success(track)

    def rip(self, session, sample_rate, frame_bytes, num_frames):
        if self.ripping.is_set():
            self.progress.update_progress(num_frames, sample_rate)
            if self.pipe is not None:
                self.pipe.write(frame_bytes)

            if self.wav_file is not None:
                self.wav_file.writeframes(frame_bytes)

            if self.pcm_file is not None:
                self.pcm_file.write(frame_bytes)

    def abort_rip(self):
        self.ripping.clear()
        self.abort.set()
Exemple #8
0
class Ripper(threading.Thread):
    audio_file = None
    pcm_file = None
    rip_proc = None
    pipe = None
    ripping = False
    finished = False
    current_playlist = None
    tracks_to_remove = []
    end_of_track = threading.Event()
    idx_digits = 3
    login_success = False
    progress = None
    dev_null = None

    def __init__(self, args):
        threading.Thread.__init__(self)

        # set to a daemon thread
        self.daemon = True

        # initalize progress meter
        self.progress = Progress(args, self)

        self.args = args
        self.logged_in = threading.Event()
        self.logged_out = threading.Event()
        self.logged_out.set()

        config = spotify.Config()

        default_dir = default_settings_dir()

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print("\n" + Fore.YELLOW + "Please copy your spotify_appkey.key to " + default_dir +
                    ", or use the --key|-k option" + Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)

        bit_rates = dict([
            ('160', BitRate.BITRATE_160K),
            ('320', BitRate.BITRATE_320K),
            ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
            self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
            self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
            self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
            self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN,
            self.on_logged_in)

        self.event_loop = spotify.EventLoop(self.session)
        self.event_loop.start()

    def run(self):
        args = self.args

        # login
        print("Logging in...")
        if args.last:
            self.login_as_last()
        elif args.user != None and args.password == None:
            password = getpass.getpass()
            self.login(args.user[0], password)
        else:
            self.login(args.user[0], args.password[0])

        if not self.login_success:
            print(Fore.RED + "Encountered issue while logging into Spotify, aborting..." + Fore.RESET)
            self.finished = True
            return

        # create track iterator
        for uri in args.uri:
            if os.path.exists(uri):
                tracks = itertools.chain(*[self.load_link(line.strip()) for line in open(uri)])
            elif uri.startswith("spotify:"):
                if args.exclude_appears_on and uri.startswith("spotify:artist:"):
                    album_uris = self.load_artist_albums(uri)
                    tracks = itertools.chain(*[self.load_link(album_uri) for album_uri in album_uris])
                else:
                    tracks = self.load_link(uri)
            else:
                tracks = self.search_query(uri)

            if args.flat_with_index and self.current_playlist:
                self.idx_digits = len(str(len(self.current_playlist.tracks)))

            tracks = list(tracks)
            self.progress.calc_total(tracks)

            if self.progress.total_size > 0:
                print("Total Download Size: " + format_size(self.progress.total_size))

            # ripping loop
            for idx, track in enumerate(tracks):
                try:
                    print('Loading track...')
                    track.load()
                    if track.availability != 1:
                        print(Fore.RED + 'Track is not available, skipping...' + Fore.RESET)
                        continue

                    self.audio_file = self.track_path(idx, track)

                    if not args.overwrite and os.path.exists(self.audio_file):
                        print(Fore.YELLOW + "Skipping " + track.link.uri + Fore.RESET)
                        print(Fore.CYAN + self.audio_file + Fore.RESET)
                        self.queue_remove_from_playlist(idx)
                        continue

                    self.session.player.load(track)
                    self.prepare_rip(idx, track)
                    self.session.player.play()

                    self.end_of_track.wait()
                    self.end_of_track.clear()

                    self.finish_rip(track)

                    # update id3v2 with metadata and embed front cover image
                    set_metadata_tags(args, self.audio_file, track)

                    # make a note of the index and remove all the
                    # tracks from the playlist when everything is done
                    self.queue_remove_from_playlist(idx)

                except spotify.Error as e:
                    print(Fore.RED + "Spotify error detected" + Fore.RESET)
                    print(str(e))
                    print("Skipping to next track...")
                    self.session.player.play(False)
                    self.clean_up_partial()
                    continue

            # actually removing the tracks from playlist
            self.remove_tracks_from_playlist()

        # logout, we are done
        self.logout()
        self.finished = True

    def load_link(self, uri):
        # ignore if the uri is just blank (e.g. from a file)
        if not uri: return iter([])

        link = self.session.get_link(uri)
        if link.type == spotify.LinkType.TRACK:
            track = link.as_track()
            return iter([track])
        elif link.type == spotify.LinkType.PLAYLIST:
            self.current_playlist = link.as_playlist()
            print('Loading playlist...')
            self.current_playlist.load()
            return iter(self.current_playlist.tracks)
        elif link.type == spotify.LinkType.STARRED:
            starred = link.as_playlist()
            print('Loading starred playlist...')
            starred.load()
            return iter(starred.tracks)
        elif link.type == spotify.LinkType.ALBUM:
            album = link.as_album()
            album_browser = album.browse()
            print('Loading album browser...')
            album_browser.load()
            return iter(album_browser.tracks)
        elif link.type == spotify.LinkType.ARTIST:
            artist = link.as_artist()
            artist_browser = artist.browse()
            print('Loading artist browser...')
            artist_browser.load()
            return iter(artist_browser.tracks)
        return iter([])

    # excludes 'appears on' albums
    def load_artist_albums(self, uri):
        def get_albums_json(offset):
            url = 'https://api.spotify.com/v1/artists/' + uri_tokens[2] + '/albums/?=album_type=album,single,compilation' + '&limit=50&offset=' + str(offset)
            print(Fore.GREEN + "Attempting to retrieve albums from Spotify's Web API" + Fore.RESET)
            print(Fore.CYAN + url + Fore.RESET)
            req = requests.get(url)
            if req.status_code == 200:
                return req.json()
            else:
                print(Fore.YELLOW + "URL returned non-200 HTTP code: " + str(req.status_code) + Fore.RESET)
            return None

        # extract artist id from uri
        uri_tokens = uri.split(':')
        if len(uri_tokens) != 3: return []

        # it is possible we won't get all the albums on the first request
        offset = 0
        album_uris = []
        total = None
        while (total is None or offset < total):
            try:
                # rate limit if not first request
                if total is None: time.sleep(1.0)
                albums = get_albums_json(offset)
                if albums is None: break

                # extract album URIs
                album_uris += [album['uri'] for album in albums['items']]
                offset = len(album_uris)
                if total is None: total = albums['total']
            except KeyError as e:
                break
        print(str(len(album_uris)) + " albums found")
        return album_uris

    def search_query(self, query):
        args = self.args

        print("Searching for query: " + query)
        try:
            result = self.session.search(query)
            result.load()
        except spotify.Error as e:
            print(str(e))
            return iter([])

        # list tracks
        print(Fore.GREEN + "Results" + Fore.RESET)
        for track_idx, track in enumerate(result.tracks):
            print("  " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET + " [" + to_ascii(args, track.album.name) + "] " + to_ascii(args, track.artists[0].name) + " - " + to_ascii(args, track.name) + " (" + str(track.popularity) + ")")

        pick = raw_input("Pick track(s) (ex 1-3,5): ")

        def get_track(i):
            if i >= 0 and i < len(result.tracks):
                return iter([result.tracks[i]])
            return iter([])

        pattern = re.compile("^[0-9 ,\-]+$")
        if pick.isdigit():
            pick = int(pick) - 1
            return get_track(pick)
        elif pick.lower() == "a" or pick.lower() == "all":
            return iter(result.tracks)
        elif pattern.match(pick):
            def range_string(comma_string):
                def hyphen_range(hyphen_string):
                    x = [int(x) - 1 for x in hyphen_string.split('-')]
                    return range(x[0], x[-1]+1)
                return itertools.chain(*[hyphen_range(r) for r in comma_string.split(',')])
            picks = sorted(set(list(range_string(pick))))
            return itertools.chain(*[get_track(p) for p in picks])

        if pick != "":
            print(Fore.RED + "Invalid selection" + Fore.RESET)
        return iter([])

    def clean_up_partial(self):
        if self.audio_file is not None and os.path.exists(self.audio_file):
            print(Fore.YELLOW + "Deleting partially ripped file" + Fore.RESET)
            rm_file(self.audio_file)

    def on_music_delivery(self, session, audio_format, frame_bytes, num_frames):
        self.rip(session, audio_format, frame_bytes, num_frames)
        return num_frames

    def on_connection_state_changed(self, session):
        if session.connection.state is spotify.ConnectionState.LOGGED_IN:
            self.login_success = True
            self.logged_in.set()
            self.logged_out.clear()
        elif session.connection.state is spotify.ConnectionState.LOGGED_OUT:
            self.logged_in.clear()
            self.logged_out.set()

    def on_logged_in(self, session, error):
        if error is spotify.ErrorType.OK:
            print("Logged in as " + session.user.display_name)
        else:
            errorMap = {
                9: "CLIENT_TOO_OLD",
                8: "UNABLE_TO_CONTACT_SERVER",
                6: "BAD_USERNAME_OR_PASSWORD",
                7: "USER_BANNED",
                15: "USER_NEEDS_PREMIUM",
                16: "OTHER_TRANSIENT",
                10: "OTHER_PERMANENT"
            }
            print("Logged in failed: " + errorMap.get(error, "UNKNOWN_ERROR_CODE: " + str(error)))
            self.login_success = False
            self.logged_in.set()

    def play_token_lost(self, session):
        print("\n"  + Fore.RED + "Play token lost, aborting..." + Fore.RESET)
        self.session.player.play(False)
        self.clean_up_partial()
        self.finished = True

    def on_end_of_track(self, session):
        self.session.player.play(False)
        self.end_of_track.set()

    def login(self, user, password):
        "login into Spotify"
        self.session.login(user, password, remember_me=True)
        self.logged_in.wait()

    def login_as_last(self):
        "login as the previous logged in user"
        try:
            self.session.relogin()
            self.logged_in.wait()
        except spotify.Error as e:
            print(str(e))

    def logout(self):
        "logout from Spotify"
        if self.logged_in.is_set():
            print('Logging out...')
            self.session.logout()
            self.logged_out.wait()
        self.event_loop.stop()

    def track_path(self, idx, track):
        args = self.args
        base_dir = norm_path(args.directory[0]) if args.directory != None else os.getcwd()

        artist = to_ascii(args, escape_filename_part(track.artists[0].name))
        album = to_ascii(args, escape_filename_part(track.album.name))
        track_name = to_ascii(args, escape_filename_part(track.name))
        extension = "." + args.output_type

        # in case the file name is too long
        def truncate(_str, max_size):
            return (_str[:max_size].strip() if len(_str) > max_size else _str)

        if args.flat:
            file_name = truncate(artist + " - " + track_name, 251) + extension
            audio_file = to_ascii(args, os.path.join(base_dir, file_name))
        elif args.flat_with_index:
            filled_idx = str(idx).zfill(self.idx_digits)
            file_name = truncate(filled_idx + " - " + artist + " - " + track_name, 251) + extension
            audio_file = to_ascii(args, os.path.join(base_dir, file_name))
        else:
            artist_t = truncate(artist, 255)
            album_t = truncate(album, 255)
            file_name = truncate(artist + " - " + track_name, 251) + extension
            audio_file = to_ascii(args, os.path.join(base_dir, artist_t, album_t, file_name))

        # create directory if it doesn't exist
        mp3_path = os.path.dirname(audio_file)
        if not os.path.exists(mp3_path):
            os.makedirs(mp3_path)

        return audio_file

    def prepare_rip(self, idx, track):
        args = self.args

        # reset progress
        self.progress.prepare_track(track)

        if self.progress.total_tracks > 1:
            print(Fore.GREEN + "[ " + str(idx + 1) + " / " + str(self.progress.total_tracks) + " ] Ripping " + track.link.uri + Fore.RESET)
        else:
            print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET)
        print(Fore.CYAN + self.audio_file + Fore.RESET)

        file_size = calc_file_size(self.args, track)
        print("Track Download Size: " + format_size(file_size))

        if args.output_type == "flac":
            self.rip_proc = Popen(["flac", "-f", "--best", "--silent", "--endian", "little", "--channels", "2", "--bps", "16", "--sample-rate", "44100", "--sign", "signed", "-o", self.audio_file, "-"], stdin=PIPE)
        elif args.output_type == "ogg":
            if args.cbr:
                self.rip_proc = Popen(["oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE)
            else:
                self.rip_proc = Popen(["oggenc", "--quiet", "--raw", "-q", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE)
        elif args.output_type == "opus":
            if args.cbr:
                self.rip_proc = Popen(["opusenc", "--quiet", "--cvbr", "--bitrate", str(int(args.bitrate) / 2), "--raw", "--raw-rate", "44100", "-", self.audio_file], stdin=PIPE)
            else:
                self.rip_proc = Popen(["opusenc", "--quiet", "--vbr", "--bitrate", args.vbr, "--raw", "--raw-rate", "44100", "-", self.audio_file], stdin=PIPE)
        elif args.output_type == "aac":
            if self.dev_null is None: self.dev_null = open(os.devnull, 'wb')
            if args.cbr:
                self.rip_proc = Popen(["faac", "-P", "-X", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null)
            else:
                self.rip_proc = Popen(["faac", "-P", "-X", "-q", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE, stdout=self.dev_null, stderr=self.dev_null)
        elif args.output_type == "m4a":
            if args.cbr:
                self.rip_proc = Popen(["fdkaac", "-S", "-R", "-w", "200000", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE)
            else:
                self.rip_proc = Popen(["fdkaac", "-S", "-R", "-w", "200000", "-m", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE)
        elif args.output_type == "mp3":
            if args.cbr:
                self.rip_proc = Popen(["lame", "--silent", "-cbr", "-b", args.bitrate, "-h", "-r", "-", self.audio_file], stdin=PIPE)
            else:
                self.rip_proc = Popen(["lame", "--silent", "-V", args.vbr, "-h", "-r", "-", self.audio_file], stdin=PIPE)
        self.pipe = self.rip_proc.stdin
        if args.pcm:
            pcm_file_name = self.audio_file[:-(len(args.output_type) + 1)] + ".pcm"
            self.pcm_file = open(pcm_file_name, 'w')
        self.ripping = True

    def finish_rip(self, track):
        self.progress.end_track()
        if self.pipe is not None:
            print(Fore.GREEN + 'Rip complete' + Fore.RESET)
            self.pipe.flush()
            self.pipe.close()

            # wait for process to end before continuing
            ret_code = self.rip_proc.wait()
            if ret_code != 0:
                print(Fore.YELLOW + "Warning: encoder returned non-zero error code " + str(ret_code) + Fore.RESET)
            self.rip_proc = None
            self.pipe = None
        if self.args.pcm:
            self.pcm_file.flush()
            os.fsync(self.pcm_file.fileno())
            self.pcm_file.close()
            self.pcm_file = None
        self.ripping = False

    def rip(self, session, audio_format, frame_bytes, num_frames):
        if self.ripping:
            self.progress.update_progress(num_frames, audio_format)
            self.pipe.write(frame_bytes);
            if self.args.pcm:
              self.pcm_file.write(frame_bytes)

    def abort(self):
        self.session.player.play(False)
        self.clean_up_partial()
        self.remove_tracks_from_playlist()
        self.logout()
        self.finished = True

    def queue_remove_from_playlist(self, idx):
        if self.args.remove_from_playlist:
            if self.current_playlist:
                if self.current_playlist.owner.canonical_name == self.session.user.canonical_name:
                    self.tracks_to_remove.append(idx)
                else:
                    print(Fore.RED + "This track will not be removed from playlist " +
                        self.current_playlist.name + " since " + self.session.user.canonical_name +
                        " is not the playlist owner..." + Fore.RESET)
            else:
                print(Fore.RED + "No playlist specified to remove this track from. " +
                        "Did you use '-r' without a playlist link?" + Fore.RESET)

    def remove_tracks_from_playlist(self):
        if self.args.remove_from_playlist and self.current_playlist and len(self.tracks_to_remove) > 0:
            print(Fore.YELLOW + "Removing successfully ripped tracks from playlist " +
                    self.current_playlist.name + "..." + Fore.RESET)

            self.current_playlist.remove_tracks(self.tracks_to_remove)
            self.session.process_events()

            while self.current_playlist.has_pending_changes:
                time.sleep(0.1)
Exemple #9
0
class Ripper(threading.Thread):
    name = 'SpotifyRipperThread'

    audio_file = None
    pcm_file = None
    wav_file = None
    rip_proc = None
    pipe = None
    current_playlist = None
    current_album = None
    current_chart = None

    login_success = False
    progress = None
    sync = None
    post = None
    web = None
    dev_null = None
    stop_time = None
    track_path_cache = {}

    rip_queue = queue.Queue()

    # threading events
    logged_in = threading.Event()
    logged_out = threading.Event()
    ripper_continue = threading.Event()
    ripping = threading.Event()
    end_of_track = threading.Event()
    finished = threading.Event()
    abort = threading.Event()
    skip = threading.Event()
    play_token_resume = threading.Event()

    def __init__(self, args):
        threading.Thread.__init__(self)

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args

        # initially logged-out
        self.logged_out.set()

        config = spotify.Config()
        default_dir = default_settings_dir()

        self.post = PostActions(args, self)
        self.web = WebAPI(args, self)

        proxy = os.environ.get('http_proxy')
        if proxy is not None:
            config.proxy = proxy

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key)
        else:
            if not path_exists(default_dir):
                os.makedirs(enc_str(default_dir))

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not path_exists(app_key_path):
                print("\n" + Fore.YELLOW +
                      "Please copy your spotify_appkey.key to " +
                      default_dir + ", or use the --key|-k option" +
                      Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings)
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        # disable scrobbling
        self.session.social.set_scrobbling(
            spotify.SocialProvider.SPOTIFY,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.FACEBOOK,
            spotify.ScrobblingState.LOCAL_DISABLED)
        self.session.social.set_scrobbling(
            spotify.SocialProvider.LASTFM,
            spotify.ScrobblingState.LOCAL_DISABLED)

        bit_rates = dict([
            ('160', BitRate.BITRATE_160K),
            ('320', BitRate.BITRATE_320K),
            ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
                        self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
                        self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
                        self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
                        self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN,
                        self.on_logged_in)

        self.event_loop = EventLoop(self.session, 0.1, self)

    def stop_event_loop(self):
        if self.event_loop.isAlive():
            self.event_loop.stop()
            self.event_loop.join()

    # executes on main thread (not SpotifyRipper thread)
    def login(self):
        args = self.args

        print("Logging in...")
        if args.last:
            self.login_as_last()

        if not self.login_success and args.user is not None:
            # remove old saved password
            self.session.forget_me()

            if args.password is None:
                password = getpass.getpass()
                self.login_as_user(args.user, password)
            else:
                self.login_as_user(args.user, args.password)

        return self.login_success

    def run(self):
        args = self.args

        # start event loop
        self.event_loop.start()

        # wait for main thread to login
        self.ripper_continue.wait()
        if self.abort.is_set():
            return

        # list of spotify URIs
        uris = args.uri

        def get_tracks_from_uri(uri):
            self.current_playlist = None
            self.current_album = None
            self.current_chart = None

            if isinstance(uri, list):
                return uri
            else:
                if (uri.startswith("spotify:artist:") and
                        (args.artist_album_type is not None or
                         args.artist_album_market is not None)):
                    album_uris = self.web.get_albums_with_filter(uri)
                    return itertools.chain(
                        *[self.load_link(album_uri) for
                          album_uri in album_uris])
                elif uri.startswith("spotify:charts:"):
                    charts = self.web.get_charts(uri)
                    if charts is not None:
                        self.current_chart = charts
                        chart_uris = charts["tracks"]
                        return itertools.chain(
                            *[self.load_link(chart_uri) for
                              chart_uri in chart_uris])
                    else:
                        return iter([])
                else:
                    return self.load_link(uri)

        # calculate total size and time
        all_tracks = []
        for uri in uris:
            tracks = list(get_tracks_from_uri(uri))

            # TODO: remove dependency on current_album, ...
            for idx, track in enumerate(tracks):

                # ignore local tracks
                if track.is_local:
                    continue

                audio_file = self.format_track_path(idx, track)
                all_tracks.append((track, audio_file))

        self.progress.calc_total(all_tracks)

        if self.progress.total_size > 0:
            print(
                "Total Download Size: " +
                format_size(self.progress.total_size))

        # create track iterator
        for uri in uris:
            if self.abort.is_set():
                break

            tracks = list(get_tracks_from_uri(uri))

            if args.playlist_sync and self.current_playlist:
                self.sync = Sync(args, self)
                self.sync.sync_playlist(self.current_playlist)

            # ripping loop
            for idx, track in enumerate(tracks):
                try:
                    self.check_stop_time()
                    self.skip.clear()

                    if self.abort.is_set():
                        break

                    print('Loading track...')
                    track.load()
                    if track.availability != 1 or track.is_local:
                        print(
                            Fore.RED + 'Track is not available, '
                                       'skipping...' + Fore.RESET)
                        self.post.log_failure(track)
                        continue

                    self.audio_file = self.format_track_path(idx, track)

                    if not args.overwrite and path_exists(self.audio_file):
                        if is_partial(self.audio_file, track):
                            print("Overwriting partial file")
                        else:
                            print(
                                Fore.YELLOW + "Skipping " +
                                track.link.uri + Fore.RESET)
                            print(Fore.CYAN + self.audio_file + Fore.RESET)
                            self.post.queue_remove_from_playlist(idx)
                            continue

                    self.session.player.load(track)
                    self.prepare_rip(idx, track)
                    self.session.player.play()

                    timeout_count = 0
                    while not self.end_of_track.is_set() or \
                            not self.rip_queue.empty():
                        try:
                            if self.abort.is_set() or self.skip.is_set():
                                break

                            rip_item = self.rip_queue.get(timeout=1)

                            if self.abort.is_set() or self.skip.is_set():
                                break

                            self.rip(self.session, rip_item[0],
                                     rip_item[1], rip_item[2])
                        except queue.Empty:
                            timeout_count += 1
                            if timeout_count > 60:
                                raise spotify.Error("Timeout while "
                                                    "ripping track")

                    if self.skip.is_set():
                        extra_line = "" if self.play_token_resume.is_set() \
                                        else "\n"
                        print(extra_line + Fore.YELLOW +
                            "User skipped track... " + Fore.RESET)
                        self.session.player.play(False)
                        self.post.clean_up_partial()
                        self.post.log_failure(track)
                        self.end_of_track.clear()
                        self.progress.end_track(show_end=False)
                        self.ripping.clear()
                        continue

                    if self.abort.is_set():
                        self.session.player.play(False)
                        self.end_of_track.set()
                        self.post.clean_up_partial()
                        self.post.log_failure(track)
                        break

                    self.end_of_track.clear()

                    self.finish_rip(track)

                    # update id3v2 with metadata and embed front cover image
                    set_metadata_tags(args, self.audio_file, idx, track, self)

                    # make a note of the index and remove all the
                    # tracks from the playlist when everything is done
                    self.post.queue_remove_from_playlist(idx)

                    # finally log success
                    self.post.log_success(track)

                except (spotify.Error, Exception) as e:
                    if isinstance(e, Exception):
                        print(Fore.RED + "Spotify error detected" + Fore.RESET)
                    print(str(e))
                    traceback.print_exc()
                    print("Skipping to next track...")
                    self.session.player.play(False)
                    self.post.clean_up_partial()
                    self.post.log_failure(track)
                    continue

            # create playlist m3u file if needed
            self.post.create_playlist_m3u(tracks)

            # create playlist wpl file if needed
            self.post.create_playlist_wpl(tracks)

            # actually removing the tracks from playlist
            self.post.remove_tracks_from_playlist()

            # remove libspotify's offline storage cache
            self.post.remove_offline_cache()

        # logout, we are done
        self.post.end_failure_log()
        self.post.print_summary()
        self.logout()
        self.stop_event_loop()
        self.finished.set()

    def check_stop_time(self):
        args = self.args

        def wait_for_resume(resume_time):
            while datetime.now() < resume_time and not self.abort.is_set():
                time.sleep(1)

        def stop_time_triggered():
            print(Fore.YELLOW + "Stop time of " +
                  self.stop_time.strftime("%H:%M") +
                  " has been triggered, stopping..." + Fore.RESET)

            if args.resume_after is not None:
                resume_time = parse_time_str(args.resume_after)
                print(Fore.YELLOW + "Script will resume at " +
                      resume_time.strftime("%H:%M") + Fore.RESET)
                wait_for_resume(resume_time)
                self.stop_time = None
            else:
                self.abort.set()

        if args.stop_after is not None:
            if self.stop_time is None:
                self.stop_time = parse_time_str(args.stop_after)
                print(Fore.YELLOW + "Script will stop after " +
                      self.stop_time.strftime("%H:%M") + Fore.RESET)

            if self.stop_time < datetime.now():
                stop_time_triggered()

        # we also wait if the "play token" was lost
        elif self.play_token_resume.is_set():
            resume_time = parse_time_str(args.play_token_resume)
            print(Fore.YELLOW + "Script will resume at " +
                  resume_time.strftime("%H:%M") + Fore.RESET)
            wait_for_resume(resume_time)
            self.play_token_resume.clear()

    def load_link(self, uri):
        # ignore if the uri is just blank (e.g. from a file)
        if not uri:
            return iter([])

        link = self.session.get_link(uri)
        if link.type == spotify.LinkType.TRACK:
            track = link.as_track()
            return iter([track])
        elif link.type == spotify.LinkType.PLAYLIST:
            self.current_playlist = link.as_playlist()
            attempt_count = 1
            while self.current_playlist is None:
                if attempt_count > 3:
                    print(Fore.RED + "Could not load playlist..." +
                          Fore.RESET)
                    return iter([])
                print("Attempt " + str(attempt_count) + " failed: Spotify " +
                      "returned None for playlist, trying again in 5 " +
                      "seconds...")
                time.sleep(5.0)
                self.current_playlist = link.as_playlist()
                attempt_count += 1

            print('Loading playlist...')
            self.current_playlist.load()
            return iter(self.current_playlist.tracks)
        elif link.type == spotify.LinkType.STARRED:
            link_user = link.as_user()

            def load_starred():
                if link_user is not None:
                    return self.session.get_starred(link_user.canonical_name)
                else:
                    return self.session.get_starred()
            starred = load_starred()

            attempt_count = 1
            while starred is None:
                if attempt_count > 3:
                    print(Fore.RED + "Could not load starred playlist..." +
                          Fore.RESET)
                    return iter([])
                print("Attempt " + str(attempt_count) + " failed: Spotify " +
                      "returned None for starred playlist, trying again in " +
                      "5 seconds...")
                time.sleep(5.0)
                starred = load_starred()
                attempt_count += 1

            print('Loading starred playlist...')
            starred.load()
            return iter(starred.tracks)
        elif link.type == spotify.LinkType.ALBUM:
            album = link.as_album()
            album_browser = album.browse()
            print('Loading album browser...')
            album_browser.load()
            self.current_album = album
            return iter(album_browser.tracks)
        elif link.type == spotify.LinkType.ARTIST:
            artist = link.as_artist()
            artist_browser = artist.browse()
            print('Loading artist browser...')
            artist_browser.load()
            return iter(artist_browser.tracks)
        return iter([])

    def search_query(self, query):
        print("Searching for query: " + query)

        try:
            result = self.session.search(query)
            result.load()
        except spotify.Error as e:
            print(str(e))
            return iter([])

        if len(result.tracks) == 0:
            print(Fore.RED + "No Results" + Fore.RESET)
            return iter([])

        # list tracks
        print(Fore.GREEN + "Results" + Fore.RESET)
        for track_idx, track in enumerate(result.tracks):
            print("  " + Fore.YELLOW + str(track_idx + 1) + Fore.RESET +
                  " [" + to_ascii(track.album.name) + "] " +
                  to_ascii(track.artists[0].name) + " - " +
                  to_ascii(track.name) +
                  " (" + str(track.popularity) + ")")

        pick = raw_input("Pick track(s) (ex 1-3,5): ")

        def get_track(i):
            if i >= 0 and i < len(result.tracks):
                return iter([result.tracks[i]])
            return iter([])

        pattern = re.compile("^[0-9 ,\-]+$")
        if pick.isdigit():
            pick = int(pick) - 1
            return get_track(pick)
        elif pick.lower() == "a" or pick.lower() == "all":
            return iter(result.tracks)
        elif pattern.match(pick):
            def range_string(comma_string):
                def hyphen_range(hyphen_string):
                    x = [int(x) - 1 for x in hyphen_string.split('-')]
                    return range(x[0], x[-1] + 1)

                return itertools.chain(
                    *[hyphen_range(r) for r in comma_string.split(',')])

            picks = sorted(set(list(range_string(pick))))
            return itertools.chain(*[get_track(p) for p in picks])

        if pick != "":
            print(Fore.RED + "Invalid selection" + Fore.RESET)
        return iter([])

    def on_music_delivery(self, session, audio_format,
                          frame_bytes, num_frames):
        try:
            self.rip_queue.put_nowait((audio_format.sample_rate,
                                       frame_bytes, num_frames))
        except queue.Full:
            print(Fore.RED + "rip_queue is full. dropped music data" +
                  Fore.RESET)
        return num_frames

    def on_connection_state_changed(self, session):
        if session.connection.state is spotify.ConnectionState.LOGGED_IN:
            self.login_success = True
            self.logged_in.set()
            self.logged_out.clear()
        elif session.connection.state is spotify.ConnectionState.LOGGED_OUT:
            self.logged_in.clear()
            self.ripper_continue.clear()
            self.logged_out.set()

    def on_logged_in(self, session, error):
        if error is spotify.ErrorType.OK:
            print("Logged in as " + session.user.display_name)
        else:
            error_map = {
                9: "CLIENT_TOO_OLD",
                8: "UNABLE_TO_CONTACT_SERVER",
                6: "BAD_USERNAME_OR_PASSWORD",
                7: "USER_BANNED",
                15: "USER_NEEDS_PREMIUM",
                16: "OTHER_TRANSIENT",
                10: "OTHER_PERMANENT"
            }
            print("Logged in failed: " +
                  error_map.get(error, "UNKNOWN_ERROR_CODE: " + str(error)))
            self.login_success = False
            self.logged_in.set()

    def play_token_lost(self, session):
        if self.args.play_token_resume is not None:
            print("\n" + Fore.RED + "Play token lost, waiting " +
                self.args.play_token_resume + " to resume..." + Fore.RESET)
            self.play_token_resume.set()
            self.skip.set()
        else:
            print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET)
            self.abort_rip()

    def on_end_of_track(self, session):
        self.session.player.play(False)
        self.end_of_track.set()

    def login_as_user(self, user, password):
        """login into Spotify"""
        self.session.login(user, password, remember_me=True)
        self.logged_in.wait()

    def login_as_last(self):
        """login as the previous logged in user"""
        try:
            self.session.relogin()
            self.logged_in.wait()
        except spotify.Error as e:
            self.login_success = False
            print(str(e))

    def logout(self):
        """logout from Spotify"""
        time.sleep(0.1)
        if self.logged_in.is_set():
            print('Logging out...')
            self.session.logout()
            self.logged_out.wait()

    def format_track_path(self, idx, track):
        args = self.args

        # check if we cached the result already
        track.load()
        if track.link.uri in self.track_path_cache:
            return self.track_path_cache[track.link.uri]

        audio_file = \
            format_track_string(self, args.format.strip(), idx, track)

        # in case the file name is too long
        def truncate(_str, max_size):
            return _str[:max_size].strip() if len(_str) > max_size else _str

        def truncate_dir_path(dir_path):
            path_tokens = dir_path.split(os.sep)
            path_tokens = [truncate(token, 255) for token in path_tokens]
            return os.sep.join(path_tokens)

        def truncate_file_name(file_name):
            tokens = file_name.rsplit(os.extsep, 1)
            if len(tokens) > 1:
                tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1)
            else:
                tokens[0] = truncate(tokens[0], 255)
            return os.extsep.join(tokens)

        # ensure each component in path is no more than 255 chars long
        if args.windows_safe:
            tokens = audio_file.rsplit(os.sep, 1)
            if len(tokens) > 1:
                audio_file = os.path.join(
                    truncate_dir_path(tokens[0]), truncate_file_name(tokens[1]))
            else:
                audio_file = truncate_file_name(tokens[0])

        # replace filename
        if args.replace is not None:
            audio_file = self.replace_filename(audio_file, args.replace)

        # remove not allowed characters in filename (windows)
        if args.windows_safe:
            audio_file = re.sub('[:"*?<>|]', '', audio_file)

        # prepend base_dir
        audio_file = to_ascii(os.path.join(base_dir(), audio_file))

        if args.normalized_ascii:
            audio_file = to_normalized_ascii(audio_file)

        # create directory if it doesn't exist
        audio_path = os.path.dirname(audio_file)
        if not path_exists(audio_path):
            os.makedirs(enc_str(audio_path))

        self.track_path_cache[track.link.uri] = audio_file
        return audio_file

    def replace_filename(self, filename, pattern_list):
        for pattern in pattern_list:
            repl = pattern.split('/')
            filename = re.sub(repl[0], repl[1], filename)
        return filename

    def prepare_rip(self, idx, track):
        args = self.args

        # reset progress
        self.progress.prepare_track(track)

        if self.progress.total_tracks > 1:
            print(Fore.GREEN + "[ " + str(self.progress.track_idx) + " / " +
                  str(self.progress.total_tracks +
                      self.progress.skipped_tracks) + " ] Ripping " +
                  track.link.uri + Fore.WHITE +
                  "\t(ESC to skip)" + Fore.RESET)
        else:
            print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET)
        print(Fore.CYAN + self.audio_file + Fore.RESET)

        file_size = calc_file_size(track)
        print("Track Download Size: " + format_size(file_size))

        if args.output_type == "wav" or args.plus_wav:
            audio_file = change_file_extension(self.audio_file, "wav") if \
                args.output_type != "wav" else self.audio_file
            wav_file = audio_file if sys.version_info >= (3, 0) \
                else enc_str(audio_file)
            self.wav_file = wave.open(wav_file, "wb")
            self.wav_file.setparams((2, 2, 44100, 0, 'NONE', 'not compressed'))

        if args.output_type == "pcm" or args.plus_pcm:
            audio_file = change_file_extension(self.audio_file, "pcm") if \
                args.output_type != "pcm" else self.audio_file
            self.pcm_file = open(enc_str(audio_file), 'wb')

        audio_file_enc = enc_str(self.audio_file)

        if args.output_type == "flac":
            self.rip_proc = Popen(
                ["flac", "-f", ("-" + str(args.comp)), "--silent", "--endian",
                 "little", "--channels", "2", "--bps", "16", "--sample-rate",
                 "44100", "--sign", "signed", "-o", audio_file_enc, "-"],
                stdin=PIPE)
        elif args.output_type == "aiff":
            self.rip_proc = Popen(
                ["sox", "-q", "--endian",
                 "little", "--channels", "2", "--bits", "16", "--rate",
                 "44100", "--encoding", "unsigned-integer", "-t", "raw",
                 "-", audio_file_enc],
                stdin=PIPE)
        elif args.output_type == "alac.m4a":
            self.rip_proc = Popen(
                ["avconv", "-nostats", "-loglevel", "0", "-f", "s16le", "-ar",
                 "44100", "-ac", "2", "-channel_layout", "stereo", "-i", "-",
                 "-acodec", "alac", audio_file_enc],
                stdin=PIPE)
        elif args.output_type == "ogg":
            if args.cbr:
                self.rip_proc = Popen(
                    ["oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o",
                     audio_file_enc, "-"], stdin=PIPE)
            else:
                self.rip_proc = Popen(
                    ["oggenc", "--quiet", "--raw", "-q", args.vbr, "-o",
                     audio_file_enc, "-"], stdin=PIPE)
        elif args.output_type == "opus":
            if args.cbr:
                self.rip_proc = Popen(
                    ["opusenc", "--quiet", "--comp", args.comp, "--cvbr",
                     "--bitrate", str(int(args.bitrate) / 2), "--raw",
                     "--raw-rate", "44100", "-", audio_file_enc], stdin=PIPE)
            else:
                self.rip_proc = Popen(
                    ["opusenc", "--quiet", "--comp", args.comp, "--vbr",
                     "--bitrate", args.vbr, "--raw", "--raw-rate", "44100",
                     "-", audio_file_enc], stdin=PIPE)
        elif args.output_type == "aac":
            if self.dev_null is None:
                self.dev_null = open(os.devnull, 'wb')
            if args.cbr:
                self.rip_proc = Popen(
                    ["faac", "-P", "-X", "-b", args.bitrate, "-o",
                     audio_file_enc, "-"], stdin=PIPE,
                    stdout=self.dev_null, stderr=self.dev_null)
            else:
                self.rip_proc = Popen(
                    ["faac", "-P", "-X", "-q", args.vbr, "-o",
                     audio_file_enc, "-"], stdin=PIPE,
                    stdout=self.dev_null, stderr=self.dev_null)
        elif args.output_type == "m4a":
            if args.cbr:
                self.rip_proc = Popen(
                    ["fdkaac", "-S", "-R", "-b",
                     args.bitrate, "-o", audio_file_enc, "-"], stdin=PIPE)
            else:
                self.rip_proc = Popen(
                    ["fdkaac", "-S", "-R", "-m", args.vbr,
                     "-o", audio_file_enc, "-"], stdin=PIPE)
        elif args.output_type == "mp3":
            lame_args = ["lame", "--silent"]

            if args.stereo_mode is not None:
                lame_args.extend(["-m", args.stereo_mode])

            if args.cbr:
                lame_args.extend(["-cbr", "-b", args.bitrate])
            else:
                lame_args.extend(["-V", args.vbr])

            lame_args.extend(["-h", "-r", "-", audio_file_enc])
            self.rip_proc = Popen(lame_args, stdin=PIPE)

        if self.rip_proc is not None:
            self.pipe = self.rip_proc.stdin

        self.ripping.set()

    def finish_rip(self, track):
        self.progress.end_track()
        if self.pipe is not None:
            print(Fore.GREEN + 'Rip complete' + Fore.RESET)
            self.pipe.flush()
            self.pipe.close()

            # wait for process to end before continuing
            ret_code = self.rip_proc.wait()
            if ret_code != 0:
                print(
                    Fore.YELLOW + "Warning: encoder returned non-zero "
                                  "error code " + str(ret_code) + Fore.RESET)
            self.rip_proc = None
            self.pipe = None

        if self.wav_file is not None:
            self.wav_file.close()
            self.wav_file = None

        if self.pcm_file is not None:
            self.pcm_file.flush()
            os.fsync(self.pcm_file.fileno())
            self.pcm_file.close()
            self.pcm_file = None

        self.ripping.clear()

    def rip(self, session, sample_rate, frame_bytes, num_frames):
        if self.ripping.is_set():
            self.progress.update_progress(num_frames, sample_rate)
            if self.pipe is not None:
                self.pipe.write(frame_bytes)

            if self.wav_file is not None:
                self.wav_file.writeframes(frame_bytes)

            if self.pcm_file is not None:
                self.pcm_file.write(frame_bytes)

    def abort_rip(self):
        self.ripping.clear()
        self.abort.set()
Exemple #10
0
    def __init__(self, args):
        threading.Thread.__init__(self)

        # set to a daemon thread
        self.daemon = True

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args
        self.logged_in = threading.Event()
        self.logged_out = threading.Event()
        self.logged_out.set()

        config = spotify.Config()

        default_dir = default_settings_dir()

        # create a log file for rip failures
        if args.fail_log is not None:
            _base_dir = base_dir(args)
            if not os.path.exists(_base_dir):
                os.makedirs(_base_dir)

            encoding = "ascii" if args.ascii else "utf-8"
            self.fail_log_file = codecs.open(os.path.join(
                _base_dir, args.fail_log[0]), 'w', encoding)

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print("\n" + Fore.YELLOW +
                      "Please copy your spotify_appkey.key to " +
                      default_dir + ", or use the --key|-k option" +
                      Fore.RESET)
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        bit_rates = dict([
            ('160', BitRate.BITRATE_160K),
            ('320', BitRate.BITRATE_320K),
            ('96', BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED,
                        self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK,
                        self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY,
                        self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST,
                        self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN,
                        self.on_logged_in)

        self.event_loop = spotify.EventLoop(self.session)
        self.event_loop.start()
Exemple #11
0
class Ripper(threading.Thread):
    audio_file = None
    pcm_file = None
    wav_file = None
    rip_proc = None
    pipe = None
    ripping = False
    finished = False
    current_playlist = None
    current_album = None
    tracks_to_remove = []
    end_of_track = threading.Event()
    idx_digits = 3
    login_success = False
    progress = None
    sync = None
    dev_null = None
    fail_log_file = None
    success_tracks = []
    failure_tracks = []

    def __init__(self, args):
        threading.Thread.__init__(self)

        # set to a daemon thread
        self.daemon = True

        # initialize progress meter
        self.progress = Progress(args, self)

        self.args = args
        self.logged_in = threading.Event()
        self.logged_out = threading.Event()
        self.logged_out.set()

        config = spotify.Config()

        default_dir = default_settings_dir()

        # create a log file for rip failures
        if args.fail_log is not None:
            _base_dir = base_dir(args)
            if not os.path.exists(_base_dir):
                os.makedirs(_base_dir)

            self.fail_log_file = open(os.path.join(_base_dir, args.fail_log[0]), "w")

        # application key location
        if args.key is not None:
            config.load_application_key_file(args.key[0])
        else:
            if not os.path.exists(default_dir):
                os.makedirs(default_dir)

            app_key_path = os.path.join(default_dir, "spotify_appkey.key")
            if not os.path.exists(app_key_path):
                print(
                    "\n"
                    + Fore.YELLOW
                    + "Please copy your spotify_appkey.key to "
                    + default_dir
                    + ", or use the --key|-k option"
                    + Fore.RESET
                )
                sys.exit(1)

            config.load_application_key_file(app_key_path)

        # settings directory
        if args.settings is not None:
            settings_dir = norm_path(args.settings[0])
            config.settings_location = settings_dir
            config.cache_location = settings_dir
        else:
            config.settings_location = default_dir
            config.cache_location = default_dir

        self.session = spotify.Session(config=config)
        self.session.volume_normalization = args.normalize

        bit_rates = dict([("160", BitRate.BITRATE_160K), ("320", BitRate.BITRATE_320K), ("96", BitRate.BITRATE_96K)])
        self.session.preferred_bitrate(bit_rates[args.quality])
        self.session.on(spotify.SessionEvent.CONNECTION_STATE_UPDATED, self.on_connection_state_changed)
        self.session.on(spotify.SessionEvent.END_OF_TRACK, self.on_end_of_track)
        self.session.on(spotify.SessionEvent.MUSIC_DELIVERY, self.on_music_delivery)
        self.session.on(spotify.SessionEvent.PLAY_TOKEN_LOST, self.play_token_lost)
        self.session.on(spotify.SessionEvent.LOGGED_IN, self.on_logged_in)

        self.event_loop = spotify.EventLoop(self.session)
        self.event_loop.start()

    def log_failure(self, track):
        self.failure_tracks.append(track)
        if self.fail_log_file is not None:
            self.fail_log_file.write(track.link.uri + "\n")

    def end_failure_log(self):
        if self.fail_log_file is not None:
            file_name = self.fail_log_file.name
            self.fail_log_file.flush()
            os.fsync(self.fail_log_file.fileno())
            self.fail_log_file.close()
            self.fail_log_file = None

            if os.path.getsize(file_name) == 0:
                rm_file(file_name)

    def print_summary(self):
        if len(self.success_tracks) + len(self.failure_tracks) <= 1:
            return

        def print_with_bullet(_str):
            if self.args.ascii:
                print(" * " + _str)
            else:
                print(" • " + _str)

        def log_tracks(tracks):
            for track in tracks:
                try:
                    track.load()
                    if len(track.artists) > 0 and track.artists[0].name is not None and track.name is not None:
                        print_with_bullet(track.artists[0].name + " - " + track.name)
                    else:
                        print_with_bullet(track.link.uri)
                except spotify.Error as e:
                    print_with_bullet(track.link.uri)
            print("")

        if len(self.success_tracks) > 0:
            print(Fore.GREEN + "\nSuccess Summary (" + str(len(self.success_tracks)) + ")\n" + ("-" * 79) + Fore.RESET)
            log_tracks(self.success_tracks)
        if len(self.failure_tracks) > 0:
            print(Fore.RED + "\nFailure Summary (" + str(len(self.failure_tracks)) + ")\n" + ("-" * 79) + Fore.RESET)
            log_tracks(self.failure_tracks)

    def create_playlist_m3u(self, tracks):
        args = self.args
        if self.current_playlist is not None and args.playlist_m3u:
            _base_dir = base_dir(args)
            playlist_path = to_ascii(args, os.path.join(_base_dir, self.current_playlist.name + ".m3u"))

            print(Fore.GREEN + "Creating playlist m3u file " + playlist_path + Fore.RESET)

            with open(playlist_path, "w") as playlist:
                for idx, track in enumerate(tracks):
                    _file = self.format_track_path(idx, track)
                    if os.path.exists(_file):
                        playlist.write(_file + "\n")

    def run(self):
        args = self.args

        # login
        print("Logging in...")
        if args.last:
            self.login_as_last()
        elif args.user is not None and args.password is None:
            password = getpass.getpass()
            self.login(args.user[0], password)
        else:
            self.login(args.user[0], args.password[0])

        if not self.login_success:
            print(Fore.RED + "Encountered issue while logging into " "Spotify, aborting..." + Fore.RESET)
            self.finished = True
            return

        # check if we were passed a file name
        if len(args.uri) == 1 and os.path.exists(args.uri[0]):
            uris = [line.strip() for line in open(args.uri[0])]
        else:
            uris = args.uri

        # create track iterator
        for uri in uris:
            if uri.startswith("spotify:"):
                if args.exclude_appears_on and uri.startswith("spotify:artist:"):
                    album_uris = self.load_artist_albums(uri)
                    tracks = itertools.chain(*[self.load_link(album_uri) for album_uri in album_uris])
                else:
                    tracks = self.load_link(uri)
            else:
                tracks = self.search_query(uri)

            if args.flat_with_index and self.current_playlist:
                self.idx_digits = len(str(len(self.current_playlist.tracks)))

            if args.playlist_sync and self.current_playlist:
                self.sync = Sync(args, self)
                self.sync.sync_playlist(self.current_playlist)

            tracks = list(tracks)
            self.progress.calc_total(tracks)

            if self.progress.total_size > 0:
                print("Total Download Size: " + format_size(self.progress.total_size))

            # ripping loop
            for idx, track in enumerate(tracks):
                try:
                    print("Loading track...")
                    track.load()
                    if track.availability != 1:
                        print(Fore.RED + "Track is not available, " "skipping..." + Fore.RESET)
                        self.log_failure(track)
                        continue

                    self.audio_file = self.format_track_path(idx, track)

                    if not args.overwrite and os.path.exists(self.audio_file):
                        print(Fore.YELLOW + "Skipping " + track.link.uri + Fore.RESET)
                        print(Fore.CYAN + self.audio_file + Fore.RESET)
                        self.queue_remove_from_playlist(idx)
                        continue

                    self.session.player.load(track)
                    self.prepare_rip(idx, track)
                    self.session.player.play()

                    self.end_of_track.wait()
                    self.end_of_track.clear()

                    self.finish_rip(track)

                    # update id3v2 with metadata and embed front cover image
                    set_metadata_tags(args, self.audio_file, track)

                    # make a note of the index and remove all the
                    # tracks from the playlist when everything is done
                    self.queue_remove_from_playlist(idx)

                except spotify.Error as e:
                    print(Fore.RED + "Spotify error detected" + Fore.RESET)
                    print(str(e))
                    print("Skipping to next track...")
                    self.session.player.play(False)
                    self.clean_up_partial()
                    self.log_failure(track)
                    continue

            # create playlist m3u file if needed
            self.create_playlist_m3u(tracks)

            # actually removing the tracks from playlist
            self.remove_tracks_from_playlist()

        # logout, we are done
        self.end_failure_log()
        self.print_summary()
        self.logout()
        self.finished = True

    def load_link(self, uri):
        # ignore if the uri is just blank (e.g. from a file)
        if not uri:
            return iter([])

        link = self.session.get_link(uri)
        if link.type == spotify.LinkType.TRACK:
            track = link.as_track()
            return iter([track])
        elif link.type == spotify.LinkType.PLAYLIST:
            self.current_playlist = link.as_playlist()
            print("Loading playlist...")
            self.current_playlist.load()
            return iter(self.current_playlist.tracks)
        elif link.type == spotify.LinkType.STARRED:
            link_user = link.as_user()
            if link_user is not None:
                starred = self.session.get_starred(link_user.canonical_name)
            else:
                starred = self.session.get_starred()

            if starred is not None:
                print("Loading starred playlist...")
                starred.load()
                return iter(starred.tracks)
            else:
                print(Fore.RED + "Could not load starred playlist..." + Fore.RESET)
                return iter([])
        elif link.type == spotify.LinkType.ALBUM:
            album = link.as_album()
            album_browser = album.browse()
            print("Loading album browser...")
            album_browser.load()
            self.current_album = album
            return iter(album_browser.tracks)
        elif link.type == spotify.LinkType.ARTIST:
            artist = link.as_artist()
            artist_browser = artist.browse()
            print("Loading artist browser...")
            artist_browser.load()
            return iter(artist_browser.tracks)
        return iter([])

    # excludes 'appears on' albums
    def load_artist_albums(self, uri):
        def get_albums_json(offset):
            url = (
                "https://api.spotify.com/v1/artists/"
                + uri_tokens[2]
                + "/albums/?=album_type=album,single,compilation"
                + "&limit=50&offset="
                + str(offset)
            )
            print(Fore.GREEN + "Attempting to retrieve albums " "from Spotify's Web API" + Fore.RESET)
            print(Fore.CYAN + url + Fore.RESET)
            req = requests.get(url)
            if req.status_code == 200:
                return req.json()
            else:
                print(Fore.YELLOW + "URL returned non-200 HTTP code: " + str(req.status_code) + Fore.RESET)
            return None

        # extract artist id from uri
        uri_tokens = uri.split(":")
        if len(uri_tokens) != 3:
            return []

        # it is possible we won't get all the albums on the first request
        offset = 0
        album_uris = []
        total = None
        while total is None or offset < total:
            try:
                # rate limit if not first request
                if total is None:
                    time.sleep(1.0)
                albums = get_albums_json(offset)
                if albums is None:
                    break

                # extract album URIs
                album_uris += [album["uri"] for album in albums["items"]]
                offset = len(album_uris)
                if total is None:
                    total = albums["total"]
            except KeyError as e:
                break
        print(str(len(album_uris)) + " albums found")
        return album_uris

    def search_query(self, query):
        args = self.args

        print("Searching for query: " + query)
        try:
            result = self.session.search(query)
            result.load()
        except spotify.Error as e:
            print(str(e))
            return iter([])

        # list tracks
        print(Fore.GREEN + "Results" + Fore.RESET)
        for track_idx, track in enumerate(result.tracks):
            print(
                "  "
                + Fore.YELLOW
                + str(track_idx + 1)
                + Fore.RESET
                + " ["
                + to_ascii(args, track.album.name)
                + "] "
                + to_ascii(args, track.artists[0].name)
                + " - "
                + to_ascii(args, track.name)
                + " ("
                + str(track.popularity)
                + ")"
            )

        pick = raw_input("Pick track(s) (ex 1-3,5): ")

        def get_track(i):
            if i >= 0 and i < len(result.tracks):
                return iter([result.tracks[i]])
            return iter([])

        pattern = re.compile("^[0-9 ,\-]+$")
        if pick.isdigit():
            pick = int(pick) - 1
            return get_track(pick)
        elif pick.lower() == "a" or pick.lower() == "all":
            return iter(result.tracks)
        elif pattern.match(pick):

            def range_string(comma_string):
                def hyphen_range(hyphen_string):
                    x = [int(x) - 1 for x in hyphen_string.split("-")]
                    return range(x[0], x[-1] + 1)

                return itertools.chain(*[hyphen_range(r) for r in comma_string.split(",")])

            picks = sorted(set(list(range_string(pick))))
            return itertools.chain(*[get_track(p) for p in picks])

        if pick != "":
            print(Fore.RED + "Invalid selection" + Fore.RESET)
        return iter([])

    def clean_up_partial(self):
        if self.audio_file is not None and os.path.exists(self.audio_file):
            print(Fore.YELLOW + "Deleting partially ripped file" + Fore.RESET)
            rm_file(self.audio_file)

    def on_music_delivery(self, session, audio_format, frame_bytes, num_frames):
        self.rip(session, audio_format, frame_bytes, num_frames)
        return num_frames

    def on_connection_state_changed(self, session):
        if session.connection.state is spotify.ConnectionState.LOGGED_IN:
            self.login_success = True
            self.logged_in.set()
            self.logged_out.clear()
        elif session.connection.state is spotify.ConnectionState.LOGGED_OUT:
            self.logged_in.clear()
            self.logged_out.set()

    def on_logged_in(self, session, error):
        if error is spotify.ErrorType.OK:
            print("Logged in as " + session.user.display_name)
        else:
            errorMap = {
                9: "CLIENT_TOO_OLD",
                8: "UNABLE_TO_CONTACT_SERVER",
                6: "BAD_USERNAME_OR_PASSWORD",
                7: "USER_BANNED",
                15: "USER_NEEDS_PREMIUM",
                16: "OTHER_TRANSIENT",
                10: "OTHER_PERMANENT",
            }
            print("Logged in failed: " + errorMap.get(error, "UNKNOWN_ERROR_CODE: " + str(error)))
            self.login_success = False
            self.logged_in.set()

    def play_token_lost(self, session):
        print("\n" + Fore.RED + "Play token lost, aborting..." + Fore.RESET)
        self.session.player.play(False)
        self.clean_up_partial()
        self.finished = True

    def on_end_of_track(self, session):
        self.session.player.play(False)
        self.end_of_track.set()

    def login(self, user, password):
        """login into Spotify"""
        self.session.login(user, password, remember_me=True)
        self.logged_in.wait()

    def login_as_last(self):
        """login as the previous logged in user"""
        try:
            self.session.relogin()
            self.logged_in.wait()
        except spotify.Error as e:
            print(str(e))

    def logout(self):
        """logout from Spotify"""
        if self.logged_in.is_set():
            print("Logging out...")
            self.session.logout()
            self.logged_out.wait()
        self.event_loop.stop()

    def album_artists_web(self, uri):
        def get_album_json(album_id):
            url = "https://api.spotify.com/v1/albums/" + album_id
            print(Fore.GREEN + "Attempting to retrieve album " "from Spotify's Web API" + Fore.RESET)
            print(Fore.CYAN + url + Fore.RESET)
            req = requests.get(url)
            if req.status_code == 200:
                return req.json()
            else:
                print(Fore.YELLOW + "URL returned non-200 HTTP code: " + str(req.status_code) + Fore.RESET)
            return None

        # extract album id from uri
        uri_tokens = uri.split(":")
        if len(uri_tokens) != 3:
            return None

        album = get_album_json(uri_tokens[2])
        if album is None:
            return None

        return [artist["name"] for artist in album["artists"]]

    def format_track_path(self, idx, track):
        args = self.args
        _base_dir = base_dir(args)
        audio_file = args.format[0].strip()

        track_artist = to_ascii(args, escape_filename_part(track.artists[0].name))
        track_artists = to_ascii(args, ", ".join([artist.name for artist in track.artists]))
        album_artist = to_ascii(
            args, self.current_album.artist.name if self.current_album is not None else track_artist
        )
        album_artists_web = track_artists

        # only retrieve album_artist_web if it exists in the format string
        if self.current_album is not None and audio_file.find("{album_artists_web}") >= 0:
            artist_array = self.album_artists_web(self.current_album.link.uri)
            if artist_array is not None:
                album_artists_web = to_ascii(args, ", ".join(artist_array))

        album = to_ascii(args, escape_filename_part(track.album.name))
        track_name = to_ascii(args, escape_filename_part(track.name))
        year = str(track.album.year)
        extension = args.output_type
        idx_str = str(idx)
        track_num = str(track.index)
        disc_num = str(track.disc)
        if self.current_playlist is not None:
            playlist_name = to_ascii(args, self.current_playlist.name)
            playlist_owner = to_ascii(args, self.current_playlist.owner.display_name)
        else:
            playlist_name = "No Playlist"
            playlist_owner = "No Playlist Owner"
        user = self.session.user.display_name

        tags = {
            "track_artist": track_artist,
            "track_artists": track_artists,
            "album_artist": album_artist,
            "album_artists_web": album_artists_web,
            "artist": track_artist,
            "artists": track_artists,
            "album": album,
            "track_name": track_name,
            "track": track_name,
            "year": year,
            "ext": extension,
            "extension": extension,
            "idx": idx_str,
            "index": idx_str,
            "track_num": track_num,
            "track_idx": track_num,
            "track_index": track_num,
            "disc_num": disc_num,
            "disc_idx": disc_num,
            "disc_index": disc_num,
            "playlist": playlist_name,
            "playlist_name": playlist_name,
            "playlist_owner": playlist_owner,
            "playlist_user": playlist_owner,
            "playlist_username": playlist_owner,
            "user": user,
            "username": user,
        }
        fill_tags = {"idx", "index", "track_num", "track_idx", "track_index", "disc_num", "disc_idx", "disc_index"}
        for tag in tags.keys():
            audio_file = audio_file.replace("{" + tag + "}", tags[tag])
            if tag in fill_tags:
                match = re.search(r"\{" + tag + r":\d+\}", audio_file)
                if match:
                    tokens = audio_file[match.start() : match.end()].strip("{}").split(":")
                    tag_filled = tags[tag].zfill(int(tokens[1]))
                    audio_file = audio_file[: match.start()] + tag_filled + audio_file[match.end() :]

        # in case the file name is too long
        def truncate(_str, max_size):
            return _str[:max_size].strip() if len(_str) > max_size else _str

        def truncate_dir_path(dir_path):
            path_tokens = dir_path.split(os.pathsep)
            path_tokens = [truncate(token, 255) for token in path_tokens]
            return os.pathsep.join(path_tokens)

        def truncate_file_name(file_name):
            tokens = file_name.rsplit(os.extsep, 1)
            if len(tokens) > 1:
                tokens[0] = truncate(tokens[0], 255 - len(tokens[1]) - 1)
            else:
                tokens[0] = truncate(tokens[0], 255)
            return os.extsep.join(tokens)

        # ensure each component in path is no more than 255 chars long
        tokens = audio_file.rsplit(os.pathsep, 1)
        if len(tokens) > 1:
            audio_file = os.path.join(truncate_dir_path(tokens[0]), truncate_file_name(tokens[1]))
        else:
            audio_file = truncate_file_name(tokens[0])

        # prepend base_dir
        audio_file = to_ascii(args, os.path.join(_base_dir, audio_file))

        # create directory if it doesn't exist
        audio_path = os.path.dirname(audio_file)
        if not os.path.exists(audio_path):
            os.makedirs(audio_path)

        return audio_file

    def prepare_rip(self, idx, track):
        args = self.args

        # reset progress
        self.progress.prepare_track(track)

        if self.progress.total_tracks > 1:
            print(
                Fore.GREEN
                + "[ "
                + str(idx + 1)
                + " / "
                + str(self.progress.total_tracks)
                + " ] Ripping "
                + track.link.uri
                + Fore.RESET
            )
        else:
            print(Fore.GREEN + "Ripping " + track.link.uri + Fore.RESET)
        print(Fore.CYAN + self.audio_file + Fore.RESET)

        file_size = calc_file_size(self.args, track)
        print("Track Download Size: " + format_size(file_size))

        if args.output_type == "wav":
            self.wav_file = wave.open(self.audio_file, "wb")
            self.wav_file.setparams((2, 2, 44100, 0, "NONE", "not compressed"))
        elif args.output_type == "pcm":
            self.pcm_file = open(self.audio_file, "wb")
        elif args.output_type == "flac":
            self.rip_proc = Popen(
                [
                    "flac",
                    "-f",
                    str("-" + args.comp),
                    "--silent",
                    "--endian",
                    "little",
                    "--channels",
                    "2",
                    "--bps",
                    "16",
                    "--sample-rate",
                    "44100",
                    "--sign",
                    "signed",
                    "-o",
                    self.audio_file,
                    "-",
                ],
                stdin=PIPE,
            )
        elif args.output_type == "ogg":
            if args.cbr:
                self.rip_proc = Popen(
                    ["oggenc", "--quiet", "--raw", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE
                )
            else:
                self.rip_proc = Popen(
                    ["oggenc", "--quiet", "--raw", "-q", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE
                )
        elif args.output_type == "opus":
            if args.cbr:
                self.rip_proc = Popen(
                    [
                        "opusenc",
                        "--quiet",
                        "--comp",
                        args.comp,
                        "--cvbr",
                        "--bitrate",
                        str(int(args.bitrate) / 2),
                        "--raw",
                        "--raw-rate",
                        "44100",
                        "-",
                        self.audio_file,
                    ],
                    stdin=PIPE,
                )
            else:
                self.rip_proc = Popen(
                    [
                        "opusenc",
                        "--quiet",
                        "--comp",
                        args.comp,
                        "--vbr",
                        "--bitrate",
                        args.vbr,
                        "--raw",
                        "--raw-rate",
                        "44100",
                        "-",
                        self.audio_file,
                    ],
                    stdin=PIPE,
                )
        elif args.output_type == "aac":
            if self.dev_null is None:
                self.dev_null = open(os.devnull, "wb")
            if args.cbr:
                self.rip_proc = Popen(
                    ["faac", "-P", "-X", "-b", args.bitrate, "-o", self.audio_file, "-"],
                    stdin=PIPE,
                    stdout=self.dev_null,
                    stderr=self.dev_null,
                )
            else:
                self.rip_proc = Popen(
                    ["faac", "-P", "-X", "-q", args.vbr, "-o", self.audio_file, "-"],
                    stdin=PIPE,
                    stdout=self.dev_null,
                    stderr=self.dev_null,
                )
        elif args.output_type == "m4a":
            if args.cbr:
                self.rip_proc = Popen(
                    ["fdkaac", "-S", "-R", "-w", "200000", "-b", args.bitrate, "-o", self.audio_file, "-"], stdin=PIPE
                )
            else:
                self.rip_proc = Popen(
                    ["fdkaac", "-S", "-R", "-w", "200000", "-m", args.vbr, "-o", self.audio_file, "-"], stdin=PIPE
                )
        elif args.output_type == "mp3":
            lame_args = ["lame", "--silent"]

            if args.stereo_mode is not None:
                lame_args.extend(["-m", args.stereo_mode])

            if args.cbr:
                lame_args.extend(["-cbr", "-b", args.bitrate])
            else:
                lame_args.extend(["-V", args.vbr])

            lame_args.extend(["-h", "-r", "-", self.audio_file])
            self.rip_proc = Popen(lame_args, stdin=PIPE)

        if self.rip_proc is not None:
            self.pipe = self.rip_proc.stdin

        self.ripping = True

    def finish_rip(self, track):
        self.progress.end_track()
        if self.pipe is not None:
            print(Fore.GREEN + "Rip complete" + Fore.RESET)
            self.pipe.flush()
            self.pipe.close()

            # wait for process to end before continuing
            ret_code = self.rip_proc.wait()
            if ret_code != 0:
                print(Fore.YELLOW + "Warning: encoder returned non-zero " "error code " + str(ret_code) + Fore.RESET)
            self.rip_proc = None
            self.pipe = None

        if self.wav_file is not None:
            self.wav_file.flush()
            os.fsync(self.wav_file.fileno())
            self.wav_file.close()
            self.wav_file = None

        if self.pcm_file is not None:
            self.pcm_file.flush()
            os.fsync(self.pcm_file.fileno())
            self.pcm_file.close()
            self.pcm_file = None

        self.ripping = False
        self.success_tracks.append(track)

    def rip(self, session, audio_format, frame_bytes, num_frames):
        if self.ripping:
            self.progress.update_progress(num_frames, audio_format)
            if self.pipe is not None:
                self.pipe.write(frame_bytes)

            if self.wav_file is not None:
                self.wav_file.writeframes(frame_bytes)

            if self.pcm_file is not None:
                self.pcm_file.write(frame_bytes)

    def abort(self):
        self.session.player.play(False)
        self.clean_up_partial()
        self.remove_tracks_from_playlist()
        self.end_failure_log()
        self.print_summary()
        self.logout()
        self.finished = True

    def queue_remove_from_playlist(self, idx):
        if self.args.remove_from_playlist:
            if self.current_playlist:
                if self.current_playlist.owner.canonical_name == self.session.user.canonical_name:
                    self.tracks_to_remove.append(idx)
                else:
                    print(
                        Fore.RED
                        + "This track will not be removed from playlist "
                        + self.current_playlist.name
                        + " since "
                        + self.session.user.canonical_name
                        + " is not the playlist owner..."
                        + Fore.RESET
                    )
            else:
                print(
                    Fore.RED
                    + "No playlist specified to remove this track from. "
                    + "Did you use '-r' without a playlist link?"
                    + Fore.RESET
                )

    def remove_tracks_from_playlist(self):
        if self.args.remove_from_playlist and self.current_playlist and len(self.tracks_to_remove) > 0:
            print(
                Fore.YELLOW
                + "Removing successfully ripped tracks from playlist "
                + self.current_playlist.name
                + "..."
                + Fore.RESET
            )

            self.current_playlist.remove_tracks(self.tracks_to_remove)
            self.session.process_events()

            while self.current_playlist.has_pending_changes:
                time.sleep(0.1)