Пример #1
0
def ask_user_for_input(question: str, abortion: str) -> str:
    try:
        user_input = input(question)
    except (KeyboardInterrupt, EOFError):
        raise ApitError(abortion)
    else:
        if not user_input:
            raise ApitError(abortion)
        return user_input
Пример #2
0
def extract_album_with_songs(metadata_json: str) -> Album:
    try:
        itunes_data = json.loads(metadata_json)
    except json.JSONDecodeError:
        raise ApitError(
            'Apple Music/iTunes Store metadata results format error')

    if 'results' not in itunes_data or 'resultCount' not in itunes_data or itunes_data[
            'resultCount'] == 0:
        raise ApitError('Apple Music/iTunes Store metadata results empty')

    return _find_album_with_songs(itunes_data['results'])
Пример #3
0
def main(options) -> int:
    configure_logging(options.verbose_level)

    logging.info('CLI options: %s', options)

    files = collect_files(options.path, FILE_FILTER)
    if len(files) == 0:
        raise ApitError('No matching files found')
    logging.info('Input path: %s', options.path)

    options.cache_path = Path(CACHE_PATH).expanduser()

    ActionType: Type[Action] = find_action_type(options.command, AVAILAIBLE_ACTIONS)

    action_options: Dict[str, Any] = ActionType.to_action_options(options)
    actions: List[Action] = [ActionType(file, action_options) for file in files]

    if any_action_needs_confirmation(actions):
        print_actions_preview(actions)
        ask_user_for_confirmation()

    for action in actions:
        action.apply()

    print_report(actions)
    return 0 if all_actions_successful(actions) else 1
Пример #4
0
def _find_album(music_data) -> Album:
    for item in music_data:
        if 'collectionType' in item and item['collectionType'] in [
                'Album', 'Compilation'
        ]:
            return Album(item)
    raise ApitError('No album found in metadata')
Пример #5
0
def fetch_store_json(url: str) -> str:
    openUrl = urllib.request.urlopen(url)
    if openUrl.getcode() != 200:
        raise ApitError(
            'Connection to Apple Music/iTunes Store failed with error code: %s'
            % openUrl.getcode())
    return openUrl.read()
Пример #6
0
def _find_atomicparsley_executable(locations) -> Path:
    for filename in locations:
        path = Path(filename).expanduser()
        if path.is_file():
            return path

    raise ApitError('AtomicParsley executable not found.')
Пример #7
0
 def not_actionable_msg(self) -> str:
     if not self.action.file_matched:
         return 'filename not matchable'
     elif self.action.options['is_original']:  # TODO refactor access
         return 'original iTunes Store file'
     elif not self.action.metadata_matched:
         return 'file not matched against metadata'
     raise ApitError('Unknown state')
Пример #8
0
def generate_store_lookup_url(user_url: str) -> str:
    match = REGEX_STORE_URL_COUNTRY_CODE_ID.match(user_url)

    if not match:
        raise ApitError(f'Invalid URL format: {user_url}')

    country_code = match.groupdict()['country_code']
    album_id = match.groupdict()['id']
    return f'https://itunes.apple.com/lookup?entity=song&country={country_code}&id={album_id}'
Пример #9
0
def _read_artwork_content(artwork_path: Path) -> mutagen.mp4.MP4Cover:
    artwork_content = artwork_path.read_bytes()
    if artwork_path.suffix == '.jpg':
        return mutagen.mp4.MP4Cover(
            artwork_content, imageformat=mutagen.mp4.MP4Cover.FORMAT_JPEG)
    elif artwork_path.suffix == '.png':
        return mutagen.mp4.MP4Cover(
            artwork_content, imageformat=mutagen.mp4.MP4Cover.FORMAT_PNG)
    raise ApitError('Unknown artwork image type')
Пример #10
0
def generate_lookup_url_by_url(source: str) -> str:
    match = REGEX_STORE_URL.match(source)

    if not match:
        raise ApitError(f'Invalid URL format: {source}')

    country_code = match.groupdict()['country_code']
    album_id = match.groupdict()['id']
    return _generate_metadata_lookup_url(album_id, country_code)
Пример #11
0
def determine_system_country_code() -> str:
    import locale
    system_language, _ = locale.getdefaultlocale()
    country_match = LANGUAGE_COUNTRY_REGEX.match(system_language)
    if not country_match:
        raise ApitError(
            'Impossible to determine system country code. Use another possibility as metadata input source'
        )
    return country_match.groupdict()['country_code']
Пример #12
0
 def status_msg(self) -> str:
     # TODO review conditions
     if not self.action.actionable:
         return f'[skipped: {self.not_actionable_msg}]'
     if not self.action.successful:
         return '[error]'
     if self.action.executed and self.action.successful:
         return 'tagged'
     raise ApitError('Invalid state')
Пример #13
0
def is_itunes_bought_file(file: Path) -> bool:
    try:
        mp4_file = read_metadata(file)
        if not mp4_file.tags:
            raise ApitError("No tags present")
    except ApitError:
        return False
    else:
        return any(map(lambda item: item in mp4_file.tags, BLACKLIST))
Пример #14
0
def download_metadata(url: str) -> str:
    try:
        with urllib.request.urlopen(url) as response:
            data_read = response.read()
            return data_read.decode('utf-8')
    except urllib.error.URLError as e:
        raise ApitError(
            'Connection to Apple Music/iTunes Store failed due to error: %s' %
            str(e))
Пример #15
0
def _escape_inner_quotes(string: str) -> str:
    match = REGEX_OUTER_QUOTE.match(string)
    if not match:
        raise ApitError(f'An error occured while escaping: {string}')
    return ''.join([
        match.groupdict()['start'],
        match.groupdict()['inner'].replace('"', '\\"'),
        match.groupdict()['end'],
    ])
Пример #16
0
 def apply(self) -> None:
     try:
         result = read_metadata(self.file)
         if not result.tags:
             raise ApitError("No tags present")
     except ApitError as e:
         self.mark_as_fail(e)
     else:
         self.mark_as_success(result)
Пример #17
0
def get_metadata_json(source: str) -> str:
    logging.info('Input source: %s', source)
    if Path(source).exists():
        logging.info('Use downloaded metadata file: %s', source)
        try:
            return Path(source).read_text()
        except Exception:
            raise ApitError('Error while reading metadata file: %s' % Path(source))
    elif is_url(source):
        logging.info('Use URL to download metadata: %s', source)
        query_url = generate_lookup_url_by_url(source)
        logging.info('Query URL: %s', query_url)
        return download_metadata(query_url)
    elif isinstance(source, str):
        logging.info('Use URL composition to download metadata: %s', source)
        query_url = generate_lookup_url_by_str(source)
        logging.info('Query URL: %s', query_url)
        return download_metadata(query_url)
    raise ApitError(f"Invalid input source: {source}")
Пример #18
0
    def add_song(self, song: Song):
        disc: int = song['discNumber']
        track: int = song['trackNumber']

        if self.has_song(disc=disc, track=track):
            raise ApitError(
                'Adding a song with duplicate disc {} and track number is impossible'
            )

        self.discs[disc][track] = song
Пример #19
0
def download_artwork(url: str) -> Tuple[bytes, MIME_TYPE]:
    try:
        with urllib.request.urlopen(url) as response:
            content_type = response.getheader('Content-Type')
            logging.info('Headers: %s', response.info())
            return response.read(), _to_mime_type(content_type)
    except urllib.error.URLError as e:
        raise ApitError(
            'Connection to Apple Music/iTunes Store failed due to error: %s' %
            str(e))
Пример #20
0
def get_metadata_json(source) -> str:
    logging.info('Input source: %s', source)
    if is_url(source):
        logging.info('Use URL to download metadata: %s', source)
        query_url = generate_store_lookup_url(source)
        logging.info('Query URL: %s', query_url)
        return fetch_store_json(query_url)
    elif Path(source).exists():
        logging.info('Use downloaded metadata file: %s', source)
        return Path(source).read_text()
    else:
        raise ApitError(f"Invalid input source: {source}")
Пример #21
0
def main(options) -> int:
    configure_logging(options.verbose_level)

    logging.info('CLI options: %s', options)

    files = collect_files(options.path, FILE_FILTER)
    if len(files) == 0:
        raise ApitError('No matching files found')
    logging.info('Input path: %s', options.path)

    options.cache_path = Path(CACHE_PATH).expanduser()

    CommandType: Type[Command] = determine_command_type(options.command)
    return CommandType().execute(files, options)
Пример #22
0
def to_pre_action_options(options) -> Mapping[str, Union[List[Song], bool]]:
    source: str = options.source

    if not source:
        source = ask_user_for_input(
            question='Input Apple Music/iTunes Store URL (starts with https://music.apple.com/...): ',
            abortion='Incompatible Apple Music/iTunes Store URL provided'
        )

    metadata_json = get_metadata_json(source)

    songs = extract_songs(metadata_json)

    first_song = songs[0]  # TODO refactor # TODO fix possible IndexError

    if options.has_search_result_cache_flag and is_url(source):
        # TODO find better location for this code
        if not len(songs):
            raise ApitError('Failed to generate a cache filename due to missing song')
        metadata_cache_file = generate_cache_filename(options.cache_path, first_song)
        save_metadata_to_cache(metadata_json, metadata_cache_file)
        logging.info('Downloaded metadata cached in: %s', metadata_cache_file)

    artwork_path = None
    if options.has_embed_artwork_flag:
        artwork_path = get_cached_artwork_path_if_exists(first_song, options)

        if artwork_path:
            logging.info('Use cached cover: %s', artwork_path)
        else:
            size = options.artwork_size
            upscaled_url = upscale_artwork_url(first_song, size)
            logging.info('Use cover link (with size %d): %s', size, upscaled_url)
            logging.info('Download cover (with size %d) from: %s', size, upscaled_url)
            if options.has_search_result_cache_flag:
                artwork_cache_path = options.cache_path
            else:
                import tempfile
                artwork_cache_path = Path(tempfile.gettempdir())
            artwork_content, image_type = download_artwork(upscaled_url)
            artwork_path = generate_artwork_filename(artwork_cache_path, first_song, image_type)
            save_artwork_to_cache(artwork_content, artwork_path)
            logging.info('Cover cached in: %s', artwork_path)

    return {
        'songs': songs,
        'should_backup': options.has_backup_flag,
        'cover_path': artwork_path,
    }
Пример #23
0
def generate_lookup_url_by_str(source: str) -> str:
    match = ID_WITH_OPTIONAL_COUNTRY_CODE_AND_SEPARATOR.match(source)

    if not match:
        raise ApitError(f'Invalid URL format: {source}')

    if match.groupdict()['country_code']:
        # user has provided country code
        country_code = match.groupdict()['country_code']
    else:
        country_code = determine_system_country_code()

    country_code = country_code.lower()
    album_id = match.groupdict()['id']
    return _generate_metadata_lookup_url(album_id, country_code)
Пример #24
0
def test_read_action_apply_error_while_reading(monkeypatch):
    error = ApitError('mock-error')

    def _raise(*args):
        raise error

    monkeypatch.setattr('apit.commands.show.action.read_metadata', _raise)
    action = ReadAction(Path('./tests/fixtures/folder-iteration/1 first.m4a'),
                        {})
    mock_mark_as_fail = MagicMock()
    monkeypatch.setattr(action, 'mark_as_fail', mock_mark_as_fail)

    action.apply()

    assert mock_mark_as_fail.call_args == call(error)
Пример #25
0
def update_metadata(file: Path,
                    song: Song,
                    cover_path: Optional[Path] = None) -> mutagen.mp4.MP4:
    mp4_file = read_metadata(file)

    if cover_path:
        artwork = _read_artwork_content(cover_path)
        _modify_mp4_file(mp4_file, song, artwork)
    else:
        _modify_mp4_file(mp4_file, song)
    # TODO error handling
    try:
        mp4_file.save()
    except Exception as e:
        raise ApitError(e)
    else:
        return mp4_file
Пример #26
0
def collect_files(path_string: str, filter_ext: Optional[Union[List[str], str]] = None) -> List[Path]:
    path = Path(path_string).expanduser()

    if not path.exists():
        raise ApitError(f'Invalid path: {path}')

    if path.is_file():
        unfiltered_files = [path]
    elif path.is_dir():
        unfiltered_files = [Path(f) for f in os.scandir(path) if f.is_file()]

    sorted_files = sorted(unfiltered_files)

    if not filter_ext:
        return sorted_files

    if isinstance(filter_ext, str):
        filter_ext = [filter_ext]

    return [f for f in sorted_files if f.suffix in filter_ext]
Пример #27
0
def test_tag_action_apply_error(monkeypatch, test_song: Song):
    error = ApitError('mock-error')

    def _raise(*args):
        raise error

    monkeypatch.setattr('apit.commands.tag.action.update_metadata', _raise)

    action = TagAction(Path('./tests/fixtures/folder-iteration/1 first.m4a'),
                       {})
    monkeypatch.setitem(action.options, 'song', test_song)
    monkeypatch.setitem(action.options, 'disc', test_song.disc_number)
    monkeypatch.setitem(action.options, 'track', test_song.track_number)
    monkeypatch.setitem(action.options, 'is_original', False)
    monkeypatch.setitem(action.options, 'should_backup', False)
    monkeypatch.setitem(action.options, 'cover_path', None)

    mock_mark_as_fail = MagicMock()
    monkeypatch.setattr(action, 'mark_as_fail', mock_mark_as_fail)

    action.apply()

    assert mock_mark_as_fail.call_args == call(error)
Пример #28
0
 def status_msg(self) -> str:
     if not self.action.successful:
         return '[error]'
     if self.action.successful:
         return 'successful'
     raise ApitError('Invalid state')  # TODO refactor
Пример #29
0
def _to_mime_type(content_type: str) -> MIME_TYPE:
    try:
        image_type = MIME_TYPE(content_type)
    except ValueError:
        raise ApitError('Unknown artwork content type: %s' % content_type)
    return image_type
Пример #30
0
def determine_command_type(command_name: str) -> Type[Command]:
    try:
        return AVAILABLE_COMMANDS[command_name]
    except KeyError:
        raise ApitError(f"Command '{command_name}' not found")