Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/manual.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/manual.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified locale/en/LC_MESSAGES/keytune.mo
Binary file not shown.
15 changes: 15 additions & 0 deletions locale/en/LC_MESSAGES/keytune.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Binary file modified locale/es/LC_MESSAGES/keytune.mo
Binary file not shown.
15 changes: 15 additions & 0 deletions locale/es/LC_MESSAGES/keytune.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
23 changes: 23 additions & 0 deletions locale/keytune.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down Expand Up @@ -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 ""
Expand Down Expand Up @@ -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 ""
Expand Down Expand Up @@ -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 ""
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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 ""

Expand Down
5 changes: 4 additions & 1 deletion src/player/frames/commands/key_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions src/player/frames/commands/open_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/player/frames/library_tabs/playback_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions src/player/frames/library_tabs/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/player/frames/playback/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
7 changes: 6 additions & 1 deletion src/player/frames/playback/media_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/player/frames/youtube_music/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
6 changes: 1 addition & 5 deletions src/player/frames/youtube_music/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/player/frames/youtube_music/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
11 changes: 10 additions & 1 deletion src/player/playlists/titles.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ 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)
if not normalized_items:
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:
Expand Down
4 changes: 3 additions & 1 deletion src/player/single_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading