diff --git a/CHANGELOG.md b/CHANGELOG.md index d53fab5..10671b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Adicionado +- **Atalho para copiar o link da mídia em execução**: `Ctrl+Shift+C` copia diretamente o link ou caminho da mídia atual para a área de transferência a qualquer momento, com anúncio via leitor de tela. + +### Corrigido +- **Consulta em tempo real de curtidas no YouTube Music**: a checagem de status ao curtir/descurtir (`Ctrl+L`) realiza consulta ao vivo à API do YouTube Music com extrator abrangente e cache em memória, identificando corretamente faixas que já haviam sido curtidas na conta do usuário. +- **Formatação limpa do título da janela**: o título da janela durante a reprodução exibe diretamente `KeyTune — [Nome da Música]`, omitindo rótulos genéricos de aba ou URLs brutas do YouTube (`watch?v=...`). + ## [1.3.0] - 2026-08-03 ### Adicionado diff --git a/docs/manual.en.md b/docs/manual.en.md index d08c2c3..aca206c 100644 --- a/docs/manual.en.md +++ b/docs/manual.en.md @@ -77,6 +77,7 @@ The shortcuts for opening media, folders, playlists, and links are in the [How t - `Ctrl+Tab` / `Ctrl+Shift+Tab`: move to the next or previous tab - `Ctrl+Shift+E`: open the equalizer for the active tab - `Ctrl+C`: copy the path or link of the selected item +- `Ctrl+Shift+C`: copy the link or path of the currently playing media - `Ctrl+Shift+S`: save the current playlist - `Ctrl+B`: switch focus between the item browser and the player - `Ctrl+F`: find an item in the current playlist or folder diff --git a/docs/manual.es.md b/docs/manual.es.md index a9f5938..9bd7517 100644 --- a/docs/manual.es.md +++ b/docs/manual.es.md @@ -77,6 +77,7 @@ Los atajos para abrir medios, carpetas, playlists y enlaces están en la secció - `Ctrl+Tab` / `Ctrl+Shift+Tab`: navegar a la pestaña siguiente o anterior - `Ctrl+Shift+E`: abrir el ecualizador de la pestaña activa - `Ctrl+C`: copiar la ruta o enlace del elemento seleccionado +- `Ctrl+Shift+C`: copiar el enlace o ruta del elemento actualmente en reproducción - `Ctrl+Shift+S`: guardar la playlist actual - `Ctrl+B`: alternar foco entre el navegador de elementos y el reproductor - `Ctrl+F`: localizar un elemento en la playlist o carpeta actual diff --git a/docs/manual.md b/docs/manual.md index 61af2cc..2806e70 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -77,6 +77,7 @@ Os atalhos para abrir mídia, pastas, playlists e links estão na seção [Como - `Ctrl+Tab` / `Ctrl+Shift+Tab`: navegar para a próxima ou aba anterior - `Ctrl+Shift+E`: abrir o equalizador da aba ativa - `Ctrl+C`: copiar o caminho ou link do item selecionado +- `Ctrl+Shift+C`: copiar o link ou caminho da mídia atual em execução - `Ctrl+Shift+S`: salvar a playlist atual - `Ctrl+B`: alternar foco entre o navegador de itens e o player - `Ctrl+F`: localizar um item na playlist ou pasta atual diff --git a/locale/en/LC_MESSAGES/keytune.mo b/locale/en/LC_MESSAGES/keytune.mo index 41ec936..82d4fed 100644 Binary files a/locale/en/LC_MESSAGES/keytune.mo and b/locale/en/LC_MESSAGES/keytune.mo differ diff --git a/locale/en/LC_MESSAGES/keytune.po b/locale/en/LC_MESSAGES/keytune.po index 6905177..ed3cc2d 100644 --- a/locale/en/LC_MESSAGES/keytune.po +++ b/locale/en/LC_MESSAGES/keytune.po @@ -3835,3 +3835,18 @@ msgstr "Austria" msgid "Índia" msgstr "India" + +msgid "Caminho da mídia atual copiado." +msgstr "Current media path copied." + +msgid "Link da mídia atual copiado." +msgstr "Current media link copied." + +msgid "Mídia do YouTube Music" +msgstr "YouTube Music item" + +msgid "Nenhuma mídia está sendo reproduzida no momento." +msgstr "No media is currently playing." + +msgid "Seleção do YouTube Music" +msgstr "YouTube Music selection" diff --git a/locale/es/LC_MESSAGES/keytune.mo b/locale/es/LC_MESSAGES/keytune.mo index ada31d9..4c7ea48 100644 Binary files a/locale/es/LC_MESSAGES/keytune.mo and b/locale/es/LC_MESSAGES/keytune.mo differ diff --git a/locale/es/LC_MESSAGES/keytune.po b/locale/es/LC_MESSAGES/keytune.po index f92dc01..2a3d1fd 100644 --- a/locale/es/LC_MESSAGES/keytune.po +++ b/locale/es/LC_MESSAGES/keytune.po @@ -3835,3 +3835,18 @@ msgstr "Austria" msgid "Índia" msgstr "India" + +msgid "Caminho da mídia atual copiado." +msgstr "Ruta del elemento actual copiada." + +msgid "Link da mídia atual copiado." +msgstr "Enlace del elemento actual copiado." + +msgid "Mídia do YouTube Music" +msgstr "Elemento de YouTube Music" + +msgid "Nenhuma mídia está sendo reproduzida no momento." +msgstr "Ningún elemento se está reproduciendo en este momento." + +msgid "Seleção do YouTube Music" +msgstr "Selección de YouTube Music" diff --git a/locale/keytune.pot b/locale/keytune.pot index c9da672..acb4289 100644 --- a/locale/keytune.pot +++ b/locale/keytune.pot @@ -1301,6 +1301,10 @@ msgstr "" msgid "Caminho copiado." msgstr "" +#: src/player/frames/commands/open_commands.py +msgid "Caminho da mídia atual copiado." +msgstr "" + #: src/player/youtube_music/auth.py msgid "Caminho de saída inválido para o arquivo de cookies." msgstr "" @@ -2629,6 +2633,10 @@ msgstr "" msgid "Link copiado." msgstr "" +#: src/player/frames/commands/open_commands.py +msgid "Link da mídia atual copiado." +msgstr "" + #: src/player/youtube_music/panel.py msgid "Link da playlist, mix ou vídeo do YouTube Music" msgstr "" @@ -2829,6 +2837,10 @@ msgstr "" msgid "Mídia atual marcada como não gostei no YouTube Music." msgstr "" +#: src/player/frames/playback/helpers.py +msgid "Mídia do YouTube Music" +msgstr "" + #: src/player/frames/library_tabs/item_removal.py msgid "Mídia fechada" msgstr "" @@ -2978,6 +2990,10 @@ msgstr "" msgid "Nenhuma mídia está carregada para avaliar." msgstr "" +#: src/player/frames/commands/open_commands.py +msgid "Nenhuma mídia está sendo reproduzida no momento." +msgstr "" + #: src/player/frames/youtube_music/playlists.py msgid "Nenhuma mídia está tocando para ser adicionada." msgstr "" @@ -3782,6 +3798,10 @@ msgid_plural "Playlist carregada: {title}. {count} itens." msgstr[0] "" msgstr[1] "" +#: src/player/youtube_music/library_manager.py +msgid "Playlist do YouTube Music" +msgstr "" + #: src/player/frames/youtube_music/lifecycle.py msgid "Playlist do YouTube Music carregada: {title}. {count} item." msgid_plural "Playlist do YouTube Music carregada: {title}. {count} itens." @@ -4388,7 +4408,10 @@ msgstr "" msgid "Seleção aberta em nova playlist: {title}." msgstr "" +#: src/player/frames/library_tabs/tabs.py #: src/player/frames/youtube_music/search.py +#: src/player/playlists/titles.py +#: src/player/youtube_music/library_manager.py msgid "Seleção do YouTube Music" msgstr "" diff --git a/src/player/frames/commands/key_navigation.py b/src/player/frames/commands/key_navigation.py index eed86da..c0f12df 100644 --- a/src/player/frames/commands/key_navigation.py +++ b/src/player/frames/commands/key_navigation.py @@ -229,7 +229,10 @@ def on_key_down(self, event): return if event.ControlDown() and not event.AltDown() and key_code in (ord("C"), ord("c")): - if not event.ShiftDown(): + if event.ShiftDown(): + self.on_copy_playing_media_path(None) + return + else: self.on_copy_current_item_path(None) return diff --git a/src/player/frames/commands/open_commands.py b/src/player/frames/commands/open_commands.py index 86cba98..1e52758 100644 --- a/src/player/frames/commands/open_commands.py +++ b/src/player/frames/commands/open_commands.py @@ -92,6 +92,23 @@ def on_copy_current_item_path(self, _event): else: self._announce(_("{count} itens copiados.").format(count=len(selected_items))) + def on_copy_playing_media_path(self, _event=None): + state = self._get_active_playlist_state() + media_path = str(getattr(state, "current_media_path", "") or "").strip() if state else "" + if not media_path: + self._announce(_("Nenhuma mídia está sendo reproduzida no momento.")) + return False + + if not self._copy_text_to_clipboard(media_path): + self._announce(_("Não foi possível acessar a área de transferência.")) + return False + + if is_remote_media_path(media_path): + self._announce(_("Link da mídia atual copiado.")) + else: + self._announce(_("Caminho da mídia atual copiado.")) + return True + def on_paste_open_from_clipboard(self, _event): if isinstance(self._get_tab_state(), ScreenTabState): return diff --git a/src/player/frames/library_tabs/playback_control.py b/src/player/frames/library_tabs/playback_control.py index 14cb586..d311867 100644 --- a/src/player/frames/library_tabs/playback_control.py +++ b/src/player/frames/library_tabs/playback_control.py @@ -133,11 +133,12 @@ def _maybe_start_automatic_crossfade(self): def _update_title(self): current_tab = self._get_tab_state() if isinstance(current_tab, ScreenTabState): - title_parts = [APP_TITLE, current_tab.title] active_state = self._get_active_playlist_state() if active_state and active_state.current_media_path: - title_parts.append(self._media_label(active_state.current_media_path)) - self.SetTitle(" — ".join(title_parts)) + media_name = self._media_label(active_state.current_media_path) + self.SetTitle(f"{APP_TITLE} — {media_name}") + else: + self.SetTitle(f"{APP_TITLE} — {current_tab.title}") return state = self._get_playlist_state() @@ -158,7 +159,7 @@ def _update_title(self): return media_name = self._media_label(state.current_media_path) - self.SetTitle(f"{APP_TITLE} — {state.title} — {media_name}") + self.SetTitle(f"{APP_TITLE} — {media_name}") refresh_smtc = getattr(self, "_refresh_smtc_state", None) if callable(refresh_smtc): refresh_smtc() diff --git a/src/player/frames/library_tabs/tabs.py b/src/player/frames/library_tabs/tabs.py index 40a25d8..c56b13c 100644 --- a/src/player/frames/library_tabs/tabs.py +++ b/src/player/frames/library_tabs/tabs.py @@ -295,6 +295,10 @@ def _open_prepared_media_playlist( state, target_index = self._resolve_target_playlist_tab() + title_str = str(title or "").strip() + if title_str.casefold().startswith("watch?v=") or "music.youtube.com/watch" in title_str.casefold() or "youtube.com/watch" in title_str.casefold(): + title = _("Seleção do YouTube Music") + state.finish_library_load() state.clear_folder_location() state.title = title diff --git a/src/player/frames/playback/helpers.py b/src/player/frames/playback/helpers.py index 5f5011a..9dafb60 100644 --- a/src/player/frames/playback/helpers.py +++ b/src/player/frames/playback/helpers.py @@ -50,6 +50,8 @@ def _default_remote_media_label(media_path): if not normalized_path: return "" media_name = os.path.basename(normalized_path) + if media_name.casefold().startswith("watch?v="): + return _("Mídia do YouTube Music") return media_name or normalized_path diff --git a/src/player/frames/playback/media_metadata.py b/src/player/frames/playback/media_metadata.py index cba101e..27aff83 100644 --- a/src/player/frames/playback/media_metadata.py +++ b/src/player/frames/playback/media_metadata.py @@ -122,7 +122,12 @@ def _apply_media_display_metadata(self, media_path, title, artist=""): state.refresh_browser_item_labels() updated = True - if len(state.items) == 1 and not state.source_path and state.title != item_label: + if ( + len(state.items) == 1 + and not state.source_path + and not is_youtube_music_media(normalized_media_path) + and state.title != item_label + ): state.title = item_label state_index = self._resolve_playlist_state_index(state) if state_index != wx.NOT_FOUND: diff --git a/src/player/frames/youtube_music/playlists.py b/src/player/frames/youtube_music/playlists.py index 4afc088..4cced6c 100644 --- a/src/player/frames/youtube_music/playlists.py +++ b/src/player/frames/youtube_music/playlists.py @@ -36,7 +36,7 @@ def _rate_current_youtube_music_media(self, rating): if not service.has_saved_browser_auth() and not self._ensure_youtube_music_authenticated(): return False - current_status = self._get_youtube_music_media_feedback_status(media_path) + current_status = self._get_youtube_music_media_feedback_status(media_path, force_refresh=True) normalized_rating = str(rating or "").strip().upper() if current_status == normalized_rating: if normalized_rating == "DISLIKE": diff --git a/src/player/frames/youtube_music/search.py b/src/player/frames/youtube_music/search.py index 00ec7e1..12de54b 100644 --- a/src/player/frames/youtube_music/search.py +++ b/src/player/frames/youtube_music/search.py @@ -99,7 +99,7 @@ def _prepare_youtube_music_search_results_for_playlist(self, search_results): for search_result in search_results: playlist_id = str(getattr(search_result, "playlist_id", "") or "").strip() if playlist_id: - playlist_content = service.get_playlist_content(playlist_id, fallback_title=search_result.title) + playlist_content = service.get_playlist_content(playlist_id, fallback_title=_("Seleção do YouTube Music")) if not playlist_content.item_urls: skipped_count += 1 continue @@ -121,10 +121,6 @@ def _prepare_youtube_music_search_results_for_playlist(self, search_results): return prepared_items, prepared_labels, playlist_result_count, skipped_count def _youtube_music_search_results_playlist_title(self, search_results): - if len(search_results) == 1: - title = str(getattr(search_results[0], "title", "") or "").strip() - if title: - return title return _("Seleção do YouTube Music") def _announce_youtube_music_playlist_addition(self, added_count, target_title, playlist_result_count, skipped_count): diff --git a/src/player/frames/youtube_music/state.py b/src/player/frames/youtube_music/state.py index dcbc09f..0c6df42 100644 --- a/src/player/frames/youtube_music/state.py +++ b/src/player/frames/youtube_music/state.py @@ -50,7 +50,7 @@ def _get_youtube_music_service(self): self._youtube_music_service = service return service - def _get_youtube_music_media_feedback_status(self, media_path): + def _get_youtube_music_media_feedback_status(self, media_path, force_refresh=False): normalized_media_path = str(media_path or "").strip() if not normalized_media_path or not is_youtube_music_media(normalized_media_path): return None @@ -60,7 +60,7 @@ def _get_youtube_music_media_feedback_status(self, media_path): return None try: - return service.get_media_feedback_status(normalized_media_path) + return service.get_media_feedback_status(normalized_media_path, force_refresh=force_refresh) except Exception: return None diff --git a/src/player/playlists/titles.py b/src/player/playlists/titles.py index 06c519d..0d712aa 100644 --- a/src/player/playlists/titles.py +++ b/src/player/playlists/titles.py @@ -9,6 +9,9 @@ def default_playlist_title(number): def build_playlist_title(items, explicit_title=None): if explicit_title: + title_str = str(explicit_title).strip() + if title_str.casefold().startswith("watch?v=") or "music.youtube.com/watch" in title_str.casefold() or "youtube.com/watch" in title_str.casefold(): + return _("Seleção do YouTube Music") return explicit_title normalized_items = list(items) @@ -16,7 +19,13 @@ def build_playlist_title(items, explicit_title=None): return default_playlist_title(1) if len(normalized_items) == 1: - return os.path.splitext(os.path.basename(normalized_items[0]))[0] + item = str(normalized_items[0]).strip() + if "watch?v=" in item.casefold() or "music.youtube.com" in item.casefold() or "youtube.com" in item.casefold(): + return _("Seleção do YouTube Music") + basename = os.path.basename(item) + if basename.casefold().startswith("watch?v="): + return _("Seleção do YouTube Music") + return os.path.splitext(basename)[0] parent_directories = {os.path.dirname(path) for path in normalized_items} if len(parent_directories) == 1: diff --git a/src/player/single_instance.py b/src/player/single_instance.py index 383e263..5eaf59d 100644 --- a/src/player/single_instance.py +++ b/src/player/single_instance.py @@ -9,13 +9,15 @@ import threading from multiprocessing.connection import Client, Listener +import os from .constants import APP_TITLE from .log import get_logger logger = get_logger(__name__) -_PIPE_ADDRESS = rf"\\.\pipe\{APP_TITLE}_SingleInstance" +_pipe_suffix = "_Dev" if os.environ.get("KEYTUNE_DEV_MODE") else "" +_PIPE_ADDRESS = rf"\\.\pipe\{APP_TITLE}_SingleInstance{_pipe_suffix}" _PIPE_AUTH_KEY = b"keytune-single-instance" ACTION_OPEN = "open" diff --git a/src/player/youtube_music/feedback_manager.py b/src/player/youtube_music/feedback_manager.py index b2cf09f..419ee94 100644 --- a/src/player/youtube_music/feedback_manager.py +++ b/src/player/youtube_music/feedback_manager.py @@ -10,6 +10,47 @@ _logger = get_logger(__name__) +def _extract_like_status_from_song_dict(song): + if not isinstance(song, dict): + return None + + candidates = [] + + video_details = song.get("videoDetails") + if isinstance(video_details, dict): + candidates.extend([ + video_details.get("likeStatus"), + video_details.get("likeState"), + video_details.get("rating"), + ]) + + candidates.extend([ + song.get("likeStatus"), + song.get("likeState"), + song.get("rating"), + ]) + + user_detail = song.get("musicItemUserDetail") + if isinstance(user_detail, dict): + candidates.extend([ + user_detail.get("likeStatus"), + user_detail.get("likeState"), + ]) + + for candidate in candidates: + if candidate is None: + continue + normalized = str(candidate).strip().upper() + if normalized in {"LIKE", "DISLIKE", "INDIFFERENT"}: + return normalized + if normalized in {"LIKED", "FAVORITE"}: + return "LIKE" + if normalized in {"DISLIKED"}: + return "DISLIKE" + + return None + + class YouTubeMusicFeedbackManager: """Handles like/dislike ratings, history reporting, and search-result saving. @@ -21,6 +62,10 @@ class YouTubeMusicFeedbackManager: def __init__(self, get_client_fn, import_module_fn): self._get_client = get_client_fn self._import_module = import_module_fn + self._feedback_cache = {} + + def clear_cache(self): + self._feedback_cache.clear() def save_search_result(self, search_result): """Save a search result (song or playlist) to the user's library.""" @@ -47,22 +92,28 @@ def save_search_result(self, search_result): raise RuntimeError(_("O resultado selecionado não pode ser salvo no YouTube Music.")) - def get_media_feedback_status(self, media_path): + def get_media_feedback_status(self, media_path, force_refresh=False): """Return the like status (``LIKE``, ``DISLIKE``, ``INDIFFERENT``) of a media item, or ``None``.""" normalized_media_path = normalize_media_path(media_path) video_id = extract_video_id_from_text(normalized_media_path) if not video_id: return None - client = self._get_client(require_auth=True) - song = client.get_song(video_id) - if not isinstance(song, dict): - return None + if not force_refresh and video_id in self._feedback_cache: + return self._feedback_cache[video_id] - like_status = str(song.get("likeStatus") or "").strip().upper() - if like_status in {"LIKE", "DISLIKE", "INDIFFERENT"}: + client = self._get_client(require_auth=True) + try: + song = client.get_song(video_id) + except Exception: + return self._feedback_cache.get(video_id) + + like_status = _extract_like_status_from_song_dict(song) + if like_status: + self._feedback_cache[video_id] = like_status return like_status - return None + + return self._feedback_cache.get(video_id) def rate_media_feedback(self, media_path, rating): """Send a like/dislike/indifferent rating for a media item.""" @@ -86,11 +137,8 @@ def rate_media_feedback(self, media_path, rating): client = self._get_client(require_auth=True) client.rate_song(video_id, like_status) + self._feedback_cache[video_id] = normalized_rating - # A API do YouTube Music propaga avaliações de forma assíncrona: o - # get_song() imediatamente após rate_song() frequentemente retorna o - # status anterior, gerando falso alarme. Se rate_song() completou - # sem exceção, a avaliação foi aceita pelo servidor. if like_status == LikeStatus.LIKE: return _("Mídia atual curtida no YouTube Music.") if like_status == LikeStatus.DISLIKE: diff --git a/src/player/youtube_music/library_manager.py b/src/player/youtube_music/library_manager.py index 4bbf701..62da714 100644 --- a/src/player/youtube_music/library_manager.py +++ b/src/player/youtube_music/library_manager.py @@ -587,11 +587,11 @@ def get_playlist_content(self, playlist_id, fallback_title="", *, require_auth=F if is_watch_playlist_id(normalized_playlist_id): playlist = client.get_watch_playlist(playlistId=normalized_playlist_id, limit=200) - playlist_title = str(fallback_title or "Mix do YouTube Music").strip() + playlist_title = _("Seleção do YouTube Music") tracks = playlist.get("tracks") or [] else: playlist = client.get_playlist(normalized_playlist_id, limit=None) - playlist_title = str(playlist.get("title") or fallback_title or "Playlist do YouTube Music").strip() + playlist_title = str(playlist.get("title") or fallback_title or _("Playlist do YouTube Music")).strip() tracks = playlist.get("tracks") or [] item_urls = [] diff --git a/src/player/youtube_music/service.py b/src/player/youtube_music/service.py index fb05bd8..9e38e35 100644 --- a/src/player/youtube_music/service.py +++ b/src/player/youtube_music/service.py @@ -175,6 +175,7 @@ def validate_saved_authentication(self): def clear_client_cache(self): self._client_provider.clear_cache() + self._feedback.clear_cache() self._account_info = None self._stream_cache_manager.clear() @@ -458,8 +459,8 @@ def get_radio_content( def save_search_result(self, search_result): return self._feedback.save_search_result(search_result) - def get_media_feedback_status(self, media_path): - return self._feedback.get_media_feedback_status(media_path) + def get_media_feedback_status(self, media_path, force_refresh=False): + return self._feedback.get_media_feedback_status(media_path, force_refresh=force_refresh) def rate_media_feedback(self, media_path, rating): return self._feedback.rate_media_feedback(media_path, rating) diff --git a/tests/test_youtube_music_service.py b/tests/test_youtube_music_service.py index 9caf278..978e093 100644 --- a/tests/test_youtube_music_service.py +++ b/tests/test_youtube_music_service.py @@ -445,9 +445,9 @@ def test_delete_playlist_rejects_watch_mix_without_api_calls(self): authenticated_client.get_playlist.assert_not_called() authenticated_client.delete_playlist.assert_not_called() - def test_get_media_feedback_status_reads_like_status_from_song(self): + def test_get_media_feedback_status_reads_like_status_from_video_details(self): authenticated_client = Mock() - authenticated_client.get_song.return_value = {"likeStatus": "LIKE"} + authenticated_client.get_song.return_value = {"videoDetails": {"likeStatus": "LIKE"}} fake_ytmusic_cls = Mock(return_value=authenticated_client) fake_module = SimpleNamespace(YTMusic=fake_ytmusic_cls) service = YouTubeMusicService() @@ -461,6 +461,23 @@ def test_get_media_feedback_status_reads_like_status_from_song(self): authenticated_client.get_song.assert_called_once_with("abc123DEF45") fake_ytmusic_cls.assert_called_once_with(service.browser_auth_file_path) + def test_get_media_feedback_status_falls_back_to_root_like_status(self): + # Fallback defensivo: se a API retornar likeStatus na raiz (formato legado), + # o status ainda deve ser lido corretamente. + authenticated_client = Mock() + authenticated_client.get_song.return_value = {"likeStatus": "INDIFFERENT"} + fake_ytmusic_cls = Mock(return_value=authenticated_client) + fake_module = SimpleNamespace(YTMusic=fake_ytmusic_cls) + service = YouTubeMusicService() + + with patch("player.youtube_music.service.import_ytmusicapi_module", return_value=fake_module), patch.object( + service, "has_saved_browser_auth", return_value=True + ): + status = service.get_media_feedback_status("https://music.youtube.com/watch?v=abc123DEF45") + + self.assertEqual(status, "INDIFFERENT") + authenticated_client.get_song.assert_called_once_with("abc123DEF45") + def test_rate_media_feedback_calls_rate_song_for_like(self): authenticated_client = Mock() fake_ytmusic_cls = Mock(return_value=authenticated_client)