mytunes-pro 1.5.2__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mytunes/app.py +389 -18
- {mytunes_pro-1.5.2.dist-info → mytunes_pro-1.8.0.dist-info}/METADATA +138 -112
- mytunes_pro-1.8.0.dist-info/RECORD +8 -0
- {mytunes_pro-1.5.2.dist-info → mytunes_pro-1.8.0.dist-info}/WHEEL +1 -1
- mytunes_pro-1.5.2.dist-info/RECORD +0 -8
- {mytunes_pro-1.5.2.dist-info → mytunes_pro-1.8.0.dist-info}/entry_points.txt +0 -0
- {mytunes_pro-1.5.2.dist-info → mytunes_pro-1.8.0.dist-info}/licenses/LICENSE +0 -0
- {mytunes_pro-1.5.2.dist-info → mytunes_pro-1.8.0.dist-info}/top_level.txt +0 -0
mytunes/app.py
CHANGED
|
@@ -16,9 +16,18 @@ import unicodedata
|
|
|
16
16
|
import socket
|
|
17
17
|
import locale
|
|
18
18
|
import signal
|
|
19
|
+
import warnings
|
|
20
|
+
# Suppress urllib3 warning about LibreSSL compatibility
|
|
21
|
+
warnings.filterwarnings("ignore", message=".*urllib3 v2 only supports OpenSSL 1.1.1+.*")
|
|
22
|
+
import webbrowser
|
|
23
|
+
import tempfile
|
|
24
|
+
import shutil
|
|
25
|
+
import pusher
|
|
26
|
+
import requests
|
|
27
|
+
|
|
19
28
|
|
|
20
29
|
# Ensure Unicode support
|
|
21
|
-
locale.setlocale(locale.LC_ALL, '')
|
|
30
|
+
# locale.setlocale(locale.LC_ALL, '')
|
|
22
31
|
|
|
23
32
|
# === [Configuration] ===
|
|
24
33
|
DATA_FILE = os.path.expanduser("~/.pymusic_data.json")
|
|
@@ -26,7 +35,7 @@ MPV_SOCKET = "/tmp/mpv_socket"
|
|
|
26
35
|
LOG_FILE = "/tmp/mytunes_mpv.log"
|
|
27
36
|
PID_FILE = "/tmp/mytunes_mpv.pid"
|
|
28
37
|
APP_NAME = "MyTunes Pro"
|
|
29
|
-
APP_VERSION = "1.
|
|
38
|
+
APP_VERSION = "1.8.0"
|
|
30
39
|
|
|
31
40
|
# === [Strings & Localization] ===
|
|
32
41
|
STRINGS = {
|
|
@@ -45,8 +54,9 @@ STRINGS = {
|
|
|
45
54
|
"stopped": "⏹ 정지됨",
|
|
46
55
|
"fav_added": "★ 즐겨찾기에 추가됨",
|
|
47
56
|
"fav_removed": "☆ 즐겨찾기 해제됨",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
57
|
+
"header_r1": "[S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [A/5]즐겨찾기추가 [Q/6]뒤로",
|
|
58
|
+
"header_r2": "[F7]유튜브 [F8]라이브 [F9]라이브공유 [SPC]Play/Stop [+/-]볼륨 [<>]빨리감기",
|
|
59
|
+
"help_guide": "[j/k]이동 [En]선택 [h/q]뒤로 [S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [F7]유튜브 [F8]라이브 [F9]라이브공유",
|
|
50
60
|
"menu_main": "☰ 메인 메뉴",
|
|
51
61
|
"menu_search_results": "⌕ YouTube 음악 검색",
|
|
52
62
|
"menu_favorites": "★ 나의 즐겨찾기",
|
|
@@ -73,8 +83,9 @@ STRINGS = {
|
|
|
73
83
|
"stopped": "⏹ Stopped",
|
|
74
84
|
"fav_added": "★ Added to Favorites",
|
|
75
85
|
"fav_removed": "☆ Removed from Favorites",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
86
|
+
"header_r1": "[S/1]Srch [F/2]Favs [R/3]Hist [M/4]Main [A/5]AddFav [Q/6]Back",
|
|
87
|
+
"header_r2": "[F7]YT [F8]Live [F9]LiveShare [SPC]Play/Stop [+/-]Vol [<>]Seek",
|
|
88
|
+
"help_guide": "[j/k]Move [En]Select [h/q]Back [S/1]Srch [F/2]Fav [R/3]Hist [M/4]Main [F7]YT [F8]Live [F9]Share",
|
|
78
89
|
"menu_main": "☰ Main Menu",
|
|
79
90
|
"menu_search_results": "⌕ Search YouTube Music",
|
|
80
91
|
"menu_favorites": "★ My Favorites",
|
|
@@ -94,16 +105,22 @@ class DataManager:
|
|
|
94
105
|
self.data = self.load_data()
|
|
95
106
|
self.favorites_set = {f['url'] for f in self.data.get('favorites', []) if 'url' in f}
|
|
96
107
|
|
|
108
|
+
# Auto-fetch country if missing
|
|
109
|
+
if 'country' not in self.data:
|
|
110
|
+
threading.Thread(target=self.fetch_country, daemon=True).start()
|
|
111
|
+
|
|
112
|
+
|
|
97
113
|
def load_data(self):
|
|
98
114
|
if not os.path.exists(DATA_FILE):
|
|
99
|
-
return {"history": [], "favorites": [], "language": "ko", "resume": {}}
|
|
115
|
+
return {"history": [], "favorites": [], "language": "ko", "resume": {}, "search_results_history": []}
|
|
100
116
|
try:
|
|
101
117
|
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
|
102
118
|
data = json.load(f)
|
|
103
119
|
if "resume" not in data: data["resume"] = {}
|
|
120
|
+
if "search_results_history" not in data: data["search_results_history"] = []
|
|
104
121
|
return data
|
|
105
122
|
except Exception:
|
|
106
|
-
return {"history": [], "favorites": [], "language": "ko", "resume": {}}
|
|
123
|
+
return {"history": [], "favorites": [], "language": "ko", "resume": {}, "search_results_history": []}
|
|
107
124
|
|
|
108
125
|
def save_data(self):
|
|
109
126
|
with open(DATA_FILE, "w", encoding="utf-8") as f:
|
|
@@ -140,6 +157,78 @@ class DataManager:
|
|
|
140
157
|
def is_favorite(self, url):
|
|
141
158
|
return url in self.favorites_set
|
|
142
159
|
|
|
160
|
+
def fetch_country(self):
|
|
161
|
+
"""Fetch country code asynchronously and save."""
|
|
162
|
+
apis = [
|
|
163
|
+
('https://ipapi.co/json/', 'country_code'),
|
|
164
|
+
('http://ip-api.com/json/', 'countryCode'),
|
|
165
|
+
('https://ipwho.is/', 'country_code')
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
for url, key in apis:
|
|
169
|
+
try:
|
|
170
|
+
resp = requests.get(url, timeout=3)
|
|
171
|
+
if resp.status_code == 200:
|
|
172
|
+
country = resp.json().get(key)
|
|
173
|
+
if country:
|
|
174
|
+
self.data['country'] = country
|
|
175
|
+
self.save_data()
|
|
176
|
+
return
|
|
177
|
+
except:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Fallback to Locale
|
|
181
|
+
try:
|
|
182
|
+
loc, _ = locale.getdefaultlocale()
|
|
183
|
+
if loc:
|
|
184
|
+
country = loc.split('_')[-1]
|
|
185
|
+
self.data['country'] = country
|
|
186
|
+
self.save_data()
|
|
187
|
+
return
|
|
188
|
+
except: pass
|
|
189
|
+
|
|
190
|
+
# Final Fallback
|
|
191
|
+
if 'country' not in self.data:
|
|
192
|
+
self.data['country'] = 'UN'
|
|
193
|
+
self.save_data()
|
|
194
|
+
|
|
195
|
+
def get_country(self):
|
|
196
|
+
# If it's US or UN, maybe it was a mistake or fallback, try to refresh once per session?
|
|
197
|
+
# Actually, let's just use what's there but allow re-fetch if requested.
|
|
198
|
+
return self.data.get('country', 'UN')
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_search_history(self):
|
|
202
|
+
return self.data.get('search_results_history', [])
|
|
203
|
+
|
|
204
|
+
def add_search_results(self, items):
|
|
205
|
+
"""Add new search results to history, deduping and limiting to 200."""
|
|
206
|
+
history = self.data.get('search_results_history', [])
|
|
207
|
+
|
|
208
|
+
# Create a set of existing URLs for fast lookup if needed,
|
|
209
|
+
# but since we want to bring duplicates to top or merge,
|
|
210
|
+
# let's just filter out any incoming items that are already in history?
|
|
211
|
+
# Requirement: "Accumulate actual result items... Dedup... Latest first"
|
|
212
|
+
|
|
213
|
+
# Strategy: Prepend new items. Remove duplicates based on URL.
|
|
214
|
+
# 1. Combine new + old
|
|
215
|
+
combined = items + history
|
|
216
|
+
|
|
217
|
+
# 2. Dedup (keep first occurrence)
|
|
218
|
+
seen_urls = set()
|
|
219
|
+
unique_history = []
|
|
220
|
+
for item in combined:
|
|
221
|
+
url = item.get('url')
|
|
222
|
+
if url and url not in seen_urls:
|
|
223
|
+
seen_urls.add(url)
|
|
224
|
+
unique_history.append(item)
|
|
225
|
+
elif not url: # Should not happen for valid items
|
|
226
|
+
unique_history.append(item)
|
|
227
|
+
|
|
228
|
+
# 3. Limit to 200
|
|
229
|
+
self.data['search_results_history'] = unique_history[:200]
|
|
230
|
+
self.save_data()
|
|
231
|
+
|
|
143
232
|
# === [Player Logic with Advanced IPC] ===
|
|
144
233
|
class Player:
|
|
145
234
|
def __init__(self):
|
|
@@ -351,6 +440,19 @@ class MyTunesApp:
|
|
|
351
440
|
signal.signal(signal.SIGHUP, self.handle_disconnect)
|
|
352
441
|
except: pass
|
|
353
442
|
|
|
443
|
+
# Pusher Client
|
|
444
|
+
try:
|
|
445
|
+
self.pusher = pusher.Pusher(
|
|
446
|
+
app_id='2106370',
|
|
447
|
+
key='44e3d7e4957944c867ec',
|
|
448
|
+
secret='0be8e65a287bbccc7369',
|
|
449
|
+
cluster='ap3',
|
|
450
|
+
ssl=True
|
|
451
|
+
)
|
|
452
|
+
except: self.pusher = None
|
|
453
|
+
self.sent_history = {}
|
|
454
|
+
|
|
455
|
+
|
|
354
456
|
def handle_disconnect(self, signum, frame):
|
|
355
457
|
"""Auto-background if terminal disconnects."""
|
|
356
458
|
self.stop_on_exit = False
|
|
@@ -582,11 +684,47 @@ class MyTunesApp:
|
|
|
582
684
|
self.player.seek(30)
|
|
583
685
|
self.status_msg = "Forward 30s"
|
|
584
686
|
|
|
585
|
-
# ESC: Background Play (Exit but keep music)
|
|
586
687
|
elif key == 27:
|
|
587
688
|
self.stop_on_exit = False
|
|
588
689
|
self.running = False
|
|
589
690
|
|
|
691
|
+
# Share Track (F9): Real-time Publish
|
|
692
|
+
elif key == curses.KEY_F9:
|
|
693
|
+
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
694
|
+
target_item = current_list[self.selection_idx]
|
|
695
|
+
url = target_item.get('url')
|
|
696
|
+
title = target_item.get('title', 'Unknown Title')
|
|
697
|
+
|
|
698
|
+
if url:
|
|
699
|
+
# If it's US, try to re-fetch country info one more time (maybe misdetected)
|
|
700
|
+
if self.dm.get_country() == 'US':
|
|
701
|
+
threading.Thread(target=self.dm.fetch_country, daemon=True).start()
|
|
702
|
+
|
|
703
|
+
# Dedup Check: Using a time-based cooldown (e.g. 5 seconds) for same URL
|
|
704
|
+
last_sent_time = self.sent_history.get(url, 0)
|
|
705
|
+
if time.time() - last_sent_time < 5:
|
|
706
|
+
self.status_msg = "⚠️ Already Shared Recently!"
|
|
707
|
+
else:
|
|
708
|
+
try:
|
|
709
|
+
# Send to Pusher
|
|
710
|
+
payload = {
|
|
711
|
+
"title": title,
|
|
712
|
+
"url": url,
|
|
713
|
+
"duration": target_item.get('duration', '--:--'),
|
|
714
|
+
"country": self.dm.get_country(),
|
|
715
|
+
"timestamp": time.time()
|
|
716
|
+
}
|
|
717
|
+
if self.pusher:
|
|
718
|
+
self.pusher.trigger('mytunes-global', 'share-track', payload)
|
|
719
|
+
self.sent_history[url] = time.time()
|
|
720
|
+
safe_title = self.truncate(title, 50)
|
|
721
|
+
self.status_msg = f"🚀 Shared: {safe_title}..."
|
|
722
|
+
else:
|
|
723
|
+
self.status_msg = "❌ Pusher Error"
|
|
724
|
+
except Exception as e:
|
|
725
|
+
self.status_msg = f"❌ Share Failed: {str(e)}"
|
|
726
|
+
|
|
727
|
+
|
|
590
728
|
# Add to Favorites: A, ㅁ, 5
|
|
591
729
|
elif k_char in ['a', 'A', 'ㅁ', '5']:
|
|
592
730
|
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
@@ -596,6 +734,138 @@ class MyTunesApp:
|
|
|
596
734
|
is_added = self.dm.toggle_favorite(target_item)
|
|
597
735
|
self.status_msg = self.t("fav_added") if is_added else self.t("fav_removed")
|
|
598
736
|
|
|
737
|
+
# Open in Browser (YouTube): F7
|
|
738
|
+
elif key == curses.KEY_F7:
|
|
739
|
+
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
740
|
+
target_item = current_list[self.selection_idx]
|
|
741
|
+
url = target_item.get('url')
|
|
742
|
+
if url:
|
|
743
|
+
if self.is_remote():
|
|
744
|
+
self.show_copy_dialog("YouTube", url)
|
|
745
|
+
else:
|
|
746
|
+
try:
|
|
747
|
+
# Robust multi-platform open
|
|
748
|
+
if sys.platform == 'darwin':
|
|
749
|
+
subprocess.Popen(["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
750
|
+
elif sys.platform == 'win32':
|
|
751
|
+
os.startfile(url)
|
|
752
|
+
elif self.is_wsl():
|
|
753
|
+
# In WSL, call the Windows shell to open the URL in Windows browser
|
|
754
|
+
subprocess.Popen(["cmd.exe", "/c", "start", url.replace("&", "^&")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
755
|
+
else:
|
|
756
|
+
webbrowser.open(url)
|
|
757
|
+
self.status_msg = "🌐 Opening YouTube in Browser..."
|
|
758
|
+
except:
|
|
759
|
+
webbrowser.open(url)
|
|
760
|
+
self.status_msg = "🌐 Opening YouTube..."
|
|
761
|
+
|
|
762
|
+
# Open Live Station: F8
|
|
763
|
+
elif key == curses.KEY_F8:
|
|
764
|
+
live_url = "https://mytunes.postgresql.co.kr/live/"
|
|
765
|
+
if self.is_remote():
|
|
766
|
+
self.show_copy_dialog("Live Station", live_url)
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
# Add timestamp to user-data-dir to force size/position flags to be respected (prevents "remembering")
|
|
770
|
+
# Using int(time.time() / 3600) to keep it stable within the same hour but fresh enough for new versions
|
|
771
|
+
temp_user_data = os.path.join(tempfile.gettempdir(), f"mytunes_v174_{int(time.time() / 10)}")
|
|
772
|
+
|
|
773
|
+
# Universal flags
|
|
774
|
+
flags = [
|
|
775
|
+
f"--app={live_url}",
|
|
776
|
+
"--window-size=712,800",
|
|
777
|
+
"--window-position=100,100",
|
|
778
|
+
f"--user-data-dir={temp_user_data}",
|
|
779
|
+
"--no-first-run",
|
|
780
|
+
"--disable-extensions",
|
|
781
|
+
"--disable-default-apps",
|
|
782
|
+
"--disable-features=Translation",
|
|
783
|
+
"--disable-save-password-bubble",
|
|
784
|
+
"--disable-translate"
|
|
785
|
+
]
|
|
786
|
+
|
|
787
|
+
launched = False
|
|
788
|
+
# 1. macOS (Avoid AppleScript to prevent permission prompts)
|
|
789
|
+
if sys.platform == 'darwin':
|
|
790
|
+
browsers = [
|
|
791
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
792
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
793
|
+
]
|
|
794
|
+
for b_path in browsers:
|
|
795
|
+
if os.path.exists(b_path):
|
|
796
|
+
try:
|
|
797
|
+
# Use 'open -na' but without AppleScript to stay 'standard' and avoid prompts
|
|
798
|
+
subprocess.Popen(["open", "-na", b_path, "--args"] + flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
799
|
+
launched = True; break
|
|
800
|
+
except: pass
|
|
801
|
+
|
|
802
|
+
# 2. Windows Native
|
|
803
|
+
elif sys.platform == 'win32':
|
|
804
|
+
win_paths = [
|
|
805
|
+
os.path.join(os.environ.get('PROGRAMFILES', 'C:\\Program Files'), 'Google\\Chrome\\Application\\chrome.exe'),
|
|
806
|
+
os.path.join(os.environ.get('PROGRAMFILES(X86)', 'C:\\Program Files (x86)'), 'Google\\Chrome\\Application\\chrome.exe'),
|
|
807
|
+
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Google\\Chrome\\Application\\chrome.exe'),
|
|
808
|
+
os.path.join(os.environ.get('PROGRAMFILES', 'C:\\Program Files'), 'BraveSoftware\\Brave-Browser\\Application\\brave.exe'),
|
|
809
|
+
os.path.join(os.environ.get('PROGRAMFILES(X86)', 'C:\\Program Files (x86)'), 'Microsoft\\Edge\\Application\\msedge.exe'),
|
|
810
|
+
os.path.join(os.environ.get('PROGRAMFILES', 'C:\\Program Files'), 'Microsoft\\Edge\\Application\\msedge.exe'),
|
|
811
|
+
]
|
|
812
|
+
# In native Windows, we remove user-data-dir to avoid permission/expansion errors
|
|
813
|
+
# app mode + new-window + window-size is sufficient.
|
|
814
|
+
win_flags = [
|
|
815
|
+
f'--app="{live_url}"',
|
|
816
|
+
'--window-size=712,800',
|
|
817
|
+
'--window-position=100,100',
|
|
818
|
+
'--new-window',
|
|
819
|
+
'--no-first-run',
|
|
820
|
+
'--disable-extensions'
|
|
821
|
+
]
|
|
822
|
+
for p in win_paths:
|
|
823
|
+
if os.path.exists(p):
|
|
824
|
+
try:
|
|
825
|
+
# Use list-based Popen for native Windows
|
|
826
|
+
subprocess.Popen([p] + win_flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
827
|
+
launched = True; break
|
|
828
|
+
except: pass
|
|
829
|
+
|
|
830
|
+
# 3. WSL (Run Windows Chrome via cmd.exe)
|
|
831
|
+
elif self.is_wsl():
|
|
832
|
+
try:
|
|
833
|
+
# Pure CMD start without user-data-dir to avoid expansion/path issues.
|
|
834
|
+
# Window sizing works reliably with just these flags.
|
|
835
|
+
c_args = [
|
|
836
|
+
f'--app=\"{live_url}\"',
|
|
837
|
+
'--window-size=712,800',
|
|
838
|
+
'--window-position=100,100',
|
|
839
|
+
'--new-window',
|
|
840
|
+
'--no-first-run',
|
|
841
|
+
'--disable-extensions'
|
|
842
|
+
]
|
|
843
|
+
# Direct call to chrome via cmd start
|
|
844
|
+
full_cmd = f'start chrome {" ".join(c_args)}'
|
|
845
|
+
subprocess.Popen(["cmd.exe", "/c", full_cmd], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
846
|
+
launched = True
|
|
847
|
+
except:
|
|
848
|
+
# Fallback to general start
|
|
849
|
+
try:
|
|
850
|
+
subprocess.Popen(["cmd.exe", "/c", "start", live_url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
851
|
+
launched = True
|
|
852
|
+
except: pass
|
|
853
|
+
|
|
854
|
+
# 4. Native Linux
|
|
855
|
+
else:
|
|
856
|
+
for b in ['google-chrome', 'google-chrome-stable', 'brave-browser', 'chromium-browser', 'chromium']:
|
|
857
|
+
p = shutil.which(b)
|
|
858
|
+
if p:
|
|
859
|
+
try:
|
|
860
|
+
subprocess.Popen([p] + flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL); launched = True; break
|
|
861
|
+
except: pass
|
|
862
|
+
|
|
863
|
+
if launched:
|
|
864
|
+
self.status_msg = "📡 Opening Live Popup (712x800)..."
|
|
865
|
+
else:
|
|
866
|
+
webbrowser.open(live_url)
|
|
867
|
+
self.status_msg = "📡 Opening Live Station (Browser)..."
|
|
868
|
+
|
|
599
869
|
def ask_resume(self, saved_time, track_title):
|
|
600
870
|
self.stdscr.nodelay(False) # Blocking input for dialog
|
|
601
871
|
h, w = self.stdscr.getmaxyx()
|
|
@@ -654,6 +924,61 @@ class MyTunesApp:
|
|
|
654
924
|
self.stdscr.refresh()
|
|
655
925
|
return res
|
|
656
926
|
|
|
927
|
+
def is_remote(self):
|
|
928
|
+
return 'SSH_CONNECTION' in os.environ or 'SSH_CLIENT' in os.environ
|
|
929
|
+
|
|
930
|
+
def is_wsl(self):
|
|
931
|
+
try:
|
|
932
|
+
if sys.platform != 'linux': return False
|
|
933
|
+
if os.path.exists('/proc/version'):
|
|
934
|
+
with open('/proc/version', 'r') as f:
|
|
935
|
+
return 'microsoft' in f.read().lower()
|
|
936
|
+
return False
|
|
937
|
+
except: return False
|
|
938
|
+
|
|
939
|
+
def show_copy_dialog(self, title, url):
|
|
940
|
+
"""Show a dialog with the URL for manual copying in remote sessions."""
|
|
941
|
+
self.stdscr.nodelay(False)
|
|
942
|
+
h, w = self.stdscr.getmaxyx()
|
|
943
|
+
box_h, box_w = 8, min(80, w - 4)
|
|
944
|
+
box_y, box_x = (h - box_h) // 2, (w - box_w) // 2
|
|
945
|
+
|
|
946
|
+
try:
|
|
947
|
+
win = curses.newwin(box_h, box_w, box_y, box_x)
|
|
948
|
+
win.keypad(True)
|
|
949
|
+
try: win.bkgd(' ', curses.color_pair(1))
|
|
950
|
+
except: pass
|
|
951
|
+
|
|
952
|
+
win.attron(curses.color_pair(1)); win.box()
|
|
953
|
+
|
|
954
|
+
# Title
|
|
955
|
+
header = " Remote Link " if self.lang == 'en' else " 원격 링크 "
|
|
956
|
+
win.addstr(0, 2, header, curses.A_BOLD | curses.color_pair(3))
|
|
957
|
+
|
|
958
|
+
# Content
|
|
959
|
+
lbl = "Open this URL in your local browser:" if self.lang == 'en' else "아래 주소를 로컬 브라우저에서여세요:"
|
|
960
|
+
win.addstr(2, 3, lbl, curses.color_pair(1))
|
|
961
|
+
|
|
962
|
+
# URL (Truncate if needed but try to show mostly)
|
|
963
|
+
disp_url = self.truncate(url, box_w - 6)
|
|
964
|
+
win.addstr(3, 3, disp_url, curses.color_pair(5) | curses.A_BOLD)
|
|
965
|
+
|
|
966
|
+
# Exit instruction
|
|
967
|
+
exit_msg = "[Enter/ESC] Close" if self.lang == 'en' else "[Enter/ESC] 닫기"
|
|
968
|
+
win.addstr(6, box_w - len(exit_msg) - 2, exit_msg, curses.color_pair(1))
|
|
969
|
+
|
|
970
|
+
win.refresh()
|
|
971
|
+
curses.flushinp()
|
|
972
|
+
|
|
973
|
+
# Wait for key
|
|
974
|
+
while True:
|
|
975
|
+
k = win.getch()
|
|
976
|
+
if k in [10, 13, curses.KEY_ENTER, 27, ord(' ')]:
|
|
977
|
+
break
|
|
978
|
+
except: pass
|
|
979
|
+
finally:
|
|
980
|
+
self.stdscr.timeout(200) # Restore non-blocking
|
|
981
|
+
|
|
657
982
|
def activate_selection(self, items):
|
|
658
983
|
if not items: return
|
|
659
984
|
item = items[self.selection_idx]
|
|
@@ -814,12 +1139,41 @@ class MyTunesApp:
|
|
|
814
1139
|
return "".join(chars).strip()
|
|
815
1140
|
|
|
816
1141
|
def prompt_search(self):
|
|
817
|
-
curses.flushinp()
|
|
1142
|
+
curses.flushinp()
|
|
1143
|
+
|
|
1144
|
+
orig_view = self.view_stack[-1]
|
|
1145
|
+
orig_results = list(self.search_results)
|
|
1146
|
+
|
|
1147
|
+
# Show search history in background using existing 'search' view
|
|
1148
|
+
history = self.dm.get_search_history()
|
|
1149
|
+
if history:
|
|
1150
|
+
self.search_results = history
|
|
1151
|
+
self.selection_idx = 0
|
|
1152
|
+
self.scroll_offset = 0
|
|
1153
|
+
if self.view_stack[-1] != "search":
|
|
1154
|
+
self.view_stack.append("search")
|
|
1155
|
+
self.status_msg = "" # Clear "List is empty" etc.
|
|
1156
|
+
self.draw()
|
|
1157
|
+
|
|
818
1158
|
query = self.input_dialog(self.t("search_label"), self.t("search_prompt"))
|
|
1159
|
+
|
|
1160
|
+
# Handling query result
|
|
1161
|
+
# Note: If user pressed ESC, input_dialog returns "" (per current implementation)
|
|
1162
|
+
# But wait, input_dialog logic: "ESC -> chars = []; break; return "".join(chars).strip()"
|
|
1163
|
+
# So ESC and empty Enter both return "".
|
|
1164
|
+
# I should check if it's possible to distinguish.
|
|
1165
|
+
|
|
819
1166
|
if query:
|
|
820
1167
|
self.status_msg = self.t("searching")
|
|
821
1168
|
self.draw()
|
|
822
1169
|
self.perform_search(query)
|
|
1170
|
+
else:
|
|
1171
|
+
# Revert if no query and we were just previewing history
|
|
1172
|
+
# But requirement 2: "If Enter with no query, preserve previous search results"
|
|
1173
|
+
# This is tricky because ESC and empty Enter currently both return "".
|
|
1174
|
+
# I will assume "" means "keep current view (history)".
|
|
1175
|
+
# If the user wants to CANCEL and go back to Main, they might need ESC.
|
|
1176
|
+
pass
|
|
823
1177
|
|
|
824
1178
|
def perform_search(self, query, page=1):
|
|
825
1179
|
try:
|
|
@@ -897,8 +1251,16 @@ class MyTunesApp:
|
|
|
897
1251
|
if self.view_stack[-1] != "search":
|
|
898
1252
|
self.view_stack.append("search")
|
|
899
1253
|
self.selection_idx = 0; self.scroll_offset = 0
|
|
1254
|
+
|
|
1255
|
+
# SAVE to History (Exclude load_more_btn)
|
|
1256
|
+
items_to_save = [x for x in new if x.get('id') != 'load_more_btn']
|
|
1257
|
+
self.dm.add_search_results(items_to_save)
|
|
1258
|
+
|
|
900
1259
|
else:
|
|
901
1260
|
self.search_results.extend(new)
|
|
1261
|
+
# Also save subsequent pages to history
|
|
1262
|
+
items_to_save = [x for x in new if x.get('id') != 'load_more_btn']
|
|
1263
|
+
self.dm.add_search_results(items_to_save)
|
|
902
1264
|
|
|
903
1265
|
self.search_page = page
|
|
904
1266
|
self.status_msg = f"Search Done. ({len(self.search_results)-1})" # -1 for button
|
|
@@ -925,14 +1287,23 @@ class MyTunesApp:
|
|
|
925
1287
|
self.stdscr.addstr(0, 0, "Window too small!")
|
|
926
1288
|
return
|
|
927
1289
|
|
|
928
|
-
# Header (
|
|
929
|
-
self.draw_box(self.stdscr, 0, 0,
|
|
1290
|
+
# Header (4 lines)
|
|
1291
|
+
self.draw_box(self.stdscr, 0, 0, 4, w, APP_NAME)
|
|
930
1292
|
title = self.t("title", APP_VERSION)
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1293
|
+
|
|
1294
|
+
# Row 1: Nav
|
|
1295
|
+
r1 = self.t("header_r1")
|
|
1296
|
+
gap1 = w - 4 - self.get_display_width(title) - self.get_display_width(r1)
|
|
1297
|
+
if gap1 < 2: gap1 = 2
|
|
1298
|
+
line1 = f"{title}{' '*gap1}{r1}"
|
|
1299
|
+
self.stdscr.addstr(1, 2, self.truncate(line1, w-4), curses.color_pair(1) | curses.A_BOLD)
|
|
1300
|
+
|
|
1301
|
+
# Row 2: Actions
|
|
1302
|
+
r2 = self.t("header_r2")
|
|
1303
|
+
gap2 = w - 4 - self.get_display_width(r2)
|
|
1304
|
+
if gap2 < 2: gap2 = 2
|
|
1305
|
+
line2 = f"{' '*gap2}{r2}"
|
|
1306
|
+
self.stdscr.addstr(2, 2, self.truncate(line2, w-4), curses.color_pair(1) | curses.A_BOLD)
|
|
936
1307
|
|
|
937
1308
|
# Footer (5 lines)
|
|
938
1309
|
footer_h = 5
|
|
@@ -977,7 +1348,7 @@ class MyTunesApp:
|
|
|
977
1348
|
self.stdscr.addstr(h - 2, 2, f"📢 {msg}", attr)
|
|
978
1349
|
|
|
979
1350
|
# List Area (Remaining Middle)
|
|
980
|
-
list_top =
|
|
1351
|
+
list_top = 4
|
|
981
1352
|
list_h = h - footer_h - list_top
|
|
982
1353
|
self.draw_box(self.stdscr, list_top, 0, list_h, w)
|
|
983
1354
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mytunes-pro
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: A lightweight, keyboard-centric terminal player for streaming YouTube music.
|
|
5
5
|
Author-email: loxo <loxo5432@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/postgresql-co-kr/mytunes
|
|
@@ -14,11 +14,12 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: requests
|
|
16
16
|
Requires-Dist: yt-dlp
|
|
17
|
+
Requires-Dist: pusher
|
|
17
18
|
Dynamic: license-file
|
|
18
19
|
|
|
19
20
|
# 🎵 MyTunes Pro (Korean)
|
|
20
21
|
|
|
21
|
-
**현대적인 CLI 유튜브 뮤직 플레이어 (v1.
|
|
22
|
+
**현대적인 CLI 유튜브 뮤직 플레이어 (v1.7.9)**
|
|
22
23
|
터미널 환경에서 **YouTube 음악을 검색하여 듣는** 가볍고 빠른 키보드 중심의 플레이어입니다.
|
|
23
24
|
한국어 입력 환경에서도 **숫자 키(1~5)**를 통해 지연 없는 쾌적한 조작이 가능합니다.
|
|
24
25
|
|
|
@@ -46,6 +47,8 @@ Dynamic: license-file
|
|
|
46
47
|
- **이어듣기**: 중단된 위치부터 **이어서 재생**할지 선택할 수 있습니다.
|
|
47
48
|
- **한글 최적화**: 한글 자소 조합 대기 시간 없이 즉시 반응하는 **숫자 단축키** 지원.
|
|
48
49
|
- **스마트 기능**: 즐겨찾기, 재생 기록(최대 100곡), 자동 음악 필터링 검색.
|
|
50
|
+
- **라이브 (F8)**: 전 세계 유저들과 함께 듣는 **실시간 음악 대시보드** (전용 팝업).
|
|
51
|
+
- **공유 (F9)**: 내가 듣는 곡을 **라이브 스테이션에 즉시 송출**하여 함께 즐깁니다.
|
|
49
52
|
- **비주얼**: 현대적인 심볼 아이콘(⌕, ★, ◷)과 깔끔한 디자인.
|
|
50
53
|
|
|
51
54
|
---
|
|
@@ -54,7 +57,7 @@ Dynamic: license-file
|
|
|
54
57
|
|
|
55
58
|
**MyTunes Pro**는 터미널(CLI) 기반 애플리케이션입니다. 각 운영체제에서 고음질 오디오를 재생하기 위해 **`mpv`**라는 엔진을 사용합니다.
|
|
56
59
|
|
|
57
|
-
- **macOS**: 터미널(iTerm2, Warp 추천)
|
|
60
|
+
- **macOS**: 터미널(iTerm2, Warp 추천) 지원. Python 3.9 이상 필요.
|
|
58
61
|
- **Linux**: 우분투, 데비안 등 모든 리눅스 배포판 지원.
|
|
59
62
|
- **Windows**: **WSL(Windows Subsystem for Linux)** 환경이 필요합니다. (아래 가이드를 참고하세요.)
|
|
60
63
|
|
|
@@ -68,9 +71,10 @@ Dynamic: license-file
|
|
|
68
71
|
자동으로 격리된 환경을 만들고 명령어를 등록해줍니다.
|
|
69
72
|
|
|
70
73
|
```bash
|
|
71
|
-
|
|
74
|
+
# pipx install 이후 명령어 등록을 위해 ensurepath 실행 시점 확인!
|
|
72
75
|
pipx install mytunes-pro
|
|
73
|
-
pipx ensurepath
|
|
76
|
+
pipx ensurepath
|
|
77
|
+
source ~/.zshrc # 또는 source ~/.bashrc (현재 터미널에 즉시 적용)
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
### 2. 일반 pip 방식
|
|
@@ -93,50 +97,50 @@ pipx upgrade mytunes-pro
|
|
|
93
97
|
|
|
94
98
|
## 🛠 환경별 요구사항 (Prerequisites)
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
실행 전 각 운영체제에 맞는 필수 도구들을 설치해 주세요.
|
|
97
101
|
|
|
98
|
-
### macOS
|
|
102
|
+
### macOS (Homebrew 사용)
|
|
99
103
|
```bash
|
|
100
|
-
brew install mpv
|
|
104
|
+
brew install mpv python3 pipx
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
### Linux (Ubuntu/Debian)
|
|
104
108
|
```bash
|
|
105
|
-
sudo apt update
|
|
109
|
+
sudo apt update
|
|
110
|
+
sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
106
111
|
```
|
|
107
112
|
|
|
108
113
|
### Windows (초보자용 WSL 가이드)
|
|
109
114
|
|
|
110
|
-
Windows
|
|
115
|
+
Windows 환경에서 한글 검색이 안 되거나 설치가 어려운 분들을 위한 가이드입니다.
|
|
111
116
|
|
|
112
117
|
> **❓ WSL이란?**
|
|
113
|
-
> 윈도우 안에서
|
|
118
|
+
> 윈도우 안에서 리눅스를 앱처럼 쓸 수 있게 해줍니다. MyTunes는 이 환경에서 완벽하게 작동합니다.
|
|
114
119
|
|
|
115
120
|
1. **WSL 설치하기**:
|
|
116
|
-
- `시작` 버튼 우클릭 -> `터미널(관리자)`
|
|
117
|
-
- 아래
|
|
121
|
+
- `시작` 버튼 우클릭 -> `터미널(관리자)` 실행.
|
|
122
|
+
- 아래 명령어 입력 후 **재부팅**:
|
|
118
123
|
```powershell
|
|
119
124
|
wsl --install -d Debian
|
|
120
125
|
```
|
|
121
|
-
- 설치가 끝나면 **컴퓨터를 다시 시작**하세요.
|
|
122
|
-
|
|
123
|
-
2. **기본 설정**:
|
|
124
|
-
- 재부팅 후 `데비안(Debian)` 창이 자동으로 뜨면, 사용할 아이디(영문)와 비밀번호를 정하세요.
|
|
125
126
|
|
|
126
127
|
3. **필수 도구 설치**:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
```
|
|
128
|
+
```bash
|
|
129
|
+
sudo apt update && sudo apt install mpv python3-pip pipx -y
|
|
130
|
+
```
|
|
131
131
|
|
|
132
132
|
4. **MyTunes 설치**:
|
|
133
|
-
|
|
133
|
+
```bash
|
|
134
|
+
pipx install mytunes-pro
|
|
135
|
+
pipx ensurepath
|
|
136
|
+
source ~/.bashrc # 설정 즉시 반영
|
|
137
|
+
```
|
|
134
138
|
|
|
135
139
|
---
|
|
136
140
|
|
|
137
141
|
## 🧑💻 개발자용 수동 설치 (Manual Installation)
|
|
138
142
|
|
|
139
|
-
직접
|
|
143
|
+
직접 소스크드를 수정하거나 개발 버전을 사용하려면 아래 과정을 따르세요.
|
|
140
144
|
|
|
141
145
|
1. **저장소 클론**:
|
|
142
146
|
```bash
|
|
@@ -156,11 +160,6 @@ Windows 환경이 익숙한 일반인도 따라할 수 있는 **완전 정복
|
|
|
156
160
|
python3 mytune.py
|
|
157
161
|
```
|
|
158
162
|
|
|
159
|
-
4. **업데이트**:
|
|
160
|
-
```bash
|
|
161
|
-
git pull
|
|
162
|
-
```
|
|
163
|
-
|
|
164
163
|
---
|
|
165
164
|
|
|
166
165
|
## ⌨️ 조작 방법 (Controls)
|
|
@@ -180,6 +179,9 @@ Windows 환경이 익숙한 일반인도 따라할 수 있는 **완전 정복
|
|
|
180
179
|
| **`5`** | **추가/삭제** | 선택한 곡 즐겨찾기 토글 (단축키 `A`와 동일) |
|
|
181
180
|
| **`+`** | **볼륨 UP** | 볼륨 +5% (단축키 `=`와 동일) |
|
|
182
181
|
| **`-`** | **볼륨 DOWN** | 볼륨 -5% (단축키 `_`와 동일) |
|
|
182
|
+
| **`F7`** | **유튜브 열기** | 현재 곡을 브라우저에서 보기 |
|
|
183
|
+
| **`F8`** | **라이브 (Live)** | **실시간 음악 대시보드 열기** (전용 팝업창) |
|
|
184
|
+
| **`F9`** | **공유 (Share)** | **현재 곡을 라이브 스테이션에 즉시 공유** |
|
|
183
185
|
| **`6`** | **뒤로가기** | 이전 화면으로 이동 (단축키 `Q`, `H`와 동일) |
|
|
184
186
|
| **`ESC`** | **배경재생** | **음악 끄지 않고 나가기** (백그라운드 재생) |
|
|
185
187
|
|
|
@@ -206,23 +208,18 @@ Windows 환경이 익숙한 일반인도 따라할 수 있는 **완전 정복
|
|
|
206
208
|
|
|
207
209
|
# 🎵 MyTunes Pro (English)
|
|
208
210
|
|
|
209
|
-
**Modern CLI YouTube Music Player (v1.
|
|
211
|
+
**Modern CLI YouTube Music Player (v1.7.9)**
|
|
210
212
|
A lightweight, keyboard-centric terminal player for streaming YouTube music.
|
|
211
|
-
Designed for speed and efficiency, with optimized controls for international keyboard imports.
|
|
212
|
-
|
|
213
|
-
> **💡 Preface**
|
|
214
|
-
> This project was created to give developers a seamless way to enjoy music without leaving their terminal environment.
|
|
215
|
-
> It basically started from a personal need to turn a **headless mini-PC running Debian Server** into a dedicated living room music station.
|
|
216
213
|
|
|
217
214
|
---
|
|
218
215
|
|
|
219
216
|
## 💻 Environment Support
|
|
220
217
|
|
|
221
|
-
**MyTunes Pro** is a Terminal-native application.
|
|
218
|
+
**MyTunes Pro** is a Terminal-native application.
|
|
222
219
|
|
|
223
|
-
- **macOS**:
|
|
220
|
+
- **macOS**: Native Terminal support. Python 3.9+ required.
|
|
224
221
|
- **Linux**: Supports all distributions (Ubuntu, Debian, etc.).
|
|
225
|
-
- **Windows**: Requires **WSL (Windows Subsystem for Linux)**.
|
|
222
|
+
- **Windows**: Requires **WSL (Windows Subsystem for Linux)**.
|
|
226
223
|
|
|
227
224
|
---
|
|
228
225
|
|
|
@@ -231,107 +228,58 @@ Designed for speed and efficiency, with optimized controls for international key
|
|
|
231
228
|
On modern macOS/Linux systems (PEP 668), using **`pipx`** is highly recommended.
|
|
232
229
|
|
|
233
230
|
### 1. Recommended (pipx)
|
|
234
|
-
Automatically manages virtual environments and global paths.
|
|
235
|
-
|
|
236
231
|
```bash
|
|
237
|
-
brew install pipx # macOS (if not installed)
|
|
238
232
|
pipx install mytunes-pro
|
|
239
|
-
pipx ensurepath
|
|
233
|
+
pipx ensurepath
|
|
234
|
+
source ~/.zshrc # or source ~/.bashrc to apply changes immediately
|
|
240
235
|
```
|
|
241
236
|
|
|
242
237
|
### 2. Standard pip
|
|
243
|
-
If you encounter an `externally-managed-environment` error, use this flag:
|
|
244
|
-
|
|
245
238
|
```bash
|
|
246
239
|
pip install mytunes-pro --break-system-packages
|
|
247
240
|
```
|
|
248
241
|
|
|
249
242
|
Run simply by typing **`mp`** in your terminal!
|
|
250
243
|
|
|
251
|
-
### 🔄 How to Update
|
|
252
|
-
If already installed, update to the latest version with one command:
|
|
253
|
-
|
|
254
|
-
```bash
|
|
255
|
-
pipx upgrade mytunes-pro
|
|
256
|
-
```
|
|
257
|
-
|
|
258
244
|
---
|
|
259
245
|
|
|
260
246
|
## 🛠 Prerequisites
|
|
261
247
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
### macOS
|
|
248
|
+
### macOS (Homebrew)
|
|
265
249
|
```bash
|
|
266
|
-
brew install mpv
|
|
250
|
+
brew install mpv python3 pipx
|
|
267
251
|
```
|
|
268
252
|
|
|
269
253
|
### Linux (Ubuntu/Debian)
|
|
270
254
|
```bash
|
|
271
|
-
sudo apt update
|
|
255
|
+
sudo apt update
|
|
256
|
+
sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
272
257
|
```
|
|
273
258
|
|
|
274
259
|
### Windows (Beginner's WSL Guide)
|
|
275
260
|
|
|
276
|
-
A step-by-step guide for Windows users to get started with CLI tools.
|
|
277
|
-
|
|
278
|
-
> **❓ What is WSL?**
|
|
279
|
-
> It stands for "Windows Subsystem for Linux". It's an official Microsoft feature that lets you run Linux powerful tools directly inside Windows like a regular app.
|
|
280
|
-
|
|
281
261
|
1. **Install WSL**:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
```powershell
|
|
285
|
-
wsl --install -d Debian
|
|
286
|
-
```
|
|
287
|
-
- **Restart your computer** after installation.
|
|
288
|
-
|
|
289
|
-
2. **Basic Setup**:
|
|
290
|
-
- After restart, the `Debian` window will pop up. Choose your username and password.
|
|
291
|
-
|
|
292
|
-
3. **Install Core Tools**:
|
|
293
|
-
- Inside the Debian window, run:
|
|
294
|
-
```bash
|
|
295
|
-
sudo apt update && sudo apt install mpv python3-pip pipx -y
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
4. **Install MyTunes**:
|
|
299
|
-
- Follow the `Quick Start` section above inside the Debian window. Done!
|
|
300
|
-
|
|
301
|
-
---
|
|
302
|
-
|
|
303
|
-
## 🧑💻 Manual Installation (For Developers)
|
|
304
|
-
|
|
305
|
-
1. **Clone Repository**:
|
|
306
|
-
```bash
|
|
307
|
-
git clone https://github.com/postgresql-co-kr/mytunes.git
|
|
308
|
-
cd mytunes
|
|
262
|
+
```powershell
|
|
263
|
+
wsl --install -d Debian
|
|
309
264
|
```
|
|
265
|
+
**Restart your computer** after installation.
|
|
310
266
|
|
|
311
|
-
2. **
|
|
267
|
+
2. **Install Core Tools**:
|
|
312
268
|
```bash
|
|
313
|
-
python3
|
|
314
|
-
source venv/bin/activate # macOS/Linux
|
|
315
|
-
pip install -r requirements.txt
|
|
269
|
+
sudo apt update && sudo apt install mpv python3-pip pipx -y
|
|
316
270
|
```
|
|
317
271
|
|
|
318
|
-
|
|
319
|
-
```bash
|
|
320
|
-
python3 mytune.py
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
4. **How to Update**:
|
|
272
|
+
4. **Install MyTunes**:
|
|
324
273
|
```bash
|
|
325
|
-
|
|
274
|
+
pipx install mytunes-pro
|
|
275
|
+
pipx ensurepath
|
|
276
|
+
source ~/.bashrc
|
|
326
277
|
```
|
|
327
278
|
|
|
328
279
|
---
|
|
329
280
|
|
|
330
281
|
## ⌨️ English Controls
|
|
331
282
|
|
|
332
|
-
### ⚡️ Instant Shortcuts (Number Keys)
|
|
333
|
-
Works instantly even with non-English keyboard layouts.
|
|
334
|
-
|
|
335
283
|
| Key | Function | Description |
|
|
336
284
|
| :--- | :--- | :--- |
|
|
337
285
|
| **`1`** | **Search** | Open search bar (Same as `S`) |
|
|
@@ -344,14 +292,92 @@ Works instantly even with non-English keyboard layouts.
|
|
|
344
292
|
| **`6`** | **Back** | Go back (Same as `Q`, `H`) |
|
|
345
293
|
| **`ESC`** | **Bg Play** | **Exit app but keep music playing** |
|
|
346
294
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 🔄 Changelog
|
|
298
|
+
|
|
299
|
+
### v1.7.9 (Latest)
|
|
300
|
+
|
|
301
|
+
- **Pure CMD-based Launch (WSL/Win)**: Final fix for WSL-to-Windows browser launch using `cmd.exe /c` with native `%LOCALAPPDATA%` expansion.
|
|
302
|
+
- **Directory Reliability**: Ensured Chrome data directory creation and access by using native Windows shell commands, eliminating the "cannot read or write" errors seen in v1.7.8.
|
|
303
|
+
- **Stable Window Sizing**: Guaranteed 712x800 window size for Live Station (F8) from WSL by correctly isolating browser profiles via native Windows paths.
|
|
304
|
+
|
|
305
|
+
### v1.7.8
|
|
306
|
+
|
|
307
|
+
- **Native PowerShell Profile Management**: Resolved directory read/write errors in WSL by moving all profile creation and path handling to the Windows side via PowerShell.
|
|
308
|
+
- **Improved Security & Isolation**: Profiles are now created in the standard Windows `LOCALAPPDATA` directory with native permissions, ensuring Chrome can always access its data.
|
|
309
|
+
- **Backslash Consistency**: Forced backslash-only paths through pure PowerShell logic, fixing the mixed-slash issue seen in WSL.
|
|
310
|
+
|
|
311
|
+
### v1.7.7
|
|
312
|
+
|
|
313
|
+
- **PowerShell Launch (WSL/Win)**: Switched to `powershell.exe` for launching browsers from WSL to ensure robust argument parsing and path handling.
|
|
314
|
+
- **Directory Fix**: Resolved "cannot read or write" error on Windows/WSL by utilizing `$env:TEMP` directly within a native shell context.
|
|
315
|
+
- **Reliable Sizing**: Guaranteed window size application by combining isolated profiles with PowerShell's superior process management.
|
|
316
|
+
|
|
317
|
+
### v1.7.6
|
|
318
|
+
|
|
319
|
+
- **Isolated Browser Profile**: Guaranteed window sizing for the Live Station (F8) on Windows/WSL by forcing an isolated browser profile using the Windows `%TEMP%` directory.
|
|
320
|
+
- **WSL Path Translation**: Implemented automatic Windows temp path resolution in WSL to enable session persistence and profile isolation.
|
|
321
|
+
|
|
322
|
+
### v1.7.5
|
|
323
|
+
|
|
324
|
+
- **WSL Integration**: Fully optimized browser launch from WSL by utilizing `cmd.exe` to trigger native Windows browsers.
|
|
325
|
+
- **F7 Windows Resolve**: Fixed an issue where YouTube (F7) wouldn't open in WSL environments.
|
|
326
|
+
- **F8 App Mode (WSL/Win)**: Enhanced flags to ensure "App Mode" (no address bar) works consistently even when launched from WSL.
|
|
327
|
+
|
|
328
|
+
### v1.7.4
|
|
329
|
+
|
|
330
|
+
- **Windows UI Refinement**: Forced Chrome "App Mode" on Windows by reordering flags and disabling extensions/default-apps to ensure a clean popup without an address bar.
|
|
331
|
+
- **Improved Isolation**: Switched to higher-frequency session rotation for Live Station (F8) to guarantee window size and position persistence fixes.
|
|
332
|
+
|
|
333
|
+
### v1.7.3
|
|
334
|
+
|
|
335
|
+
- **Windows Fixes**: Resolved issue where F7 (YouTube) failed to open browsers on Windows by implementing `os.startfile` logic.
|
|
336
|
+
- **F8 Initialization**: Improved Live Station (F8) window sizing on Windows by forcing a clean session state.
|
|
337
|
+
- **Robustness**: Enhanced cross-platform browser redirection logic to ensure consistent behavior.
|
|
338
|
+
|
|
339
|
+
### v1.7.2
|
|
340
|
+
|
|
341
|
+
- **Windows Optimization**: Fixed an issue where the Live Station (F8) window size was not correctly applied on Windows.
|
|
342
|
+
- **Improved Browser Support**: Added Microsoft Edge to the automatic browser detection list.
|
|
343
|
+
- **Robust Launch Logic**: Enhanced browser internal flags for a better initial window experience.
|
|
344
|
+
|
|
345
|
+
### v1.7.1
|
|
346
|
+
|
|
347
|
+
- **Performance & Logic Optimization**: Standardized browser launch logic for Live Station (F8) across Mac, Windows, and Linux.
|
|
348
|
+
- **UI Polish**: Silenced browser launch warnings in the terminal and added professional UI flags (disable translation/bubble) for a cleaner experience.
|
|
349
|
+
- **Improved Popup Behavior**: Optimized web interface to reuse the same window for Live Station, matching CLI application behavior.
|
|
350
|
+
- **Global Sync**: Version 1.7.1 synchronization across all platforms.
|
|
351
|
+
|
|
352
|
+
### v1.6.0
|
|
353
|
+
|
|
354
|
+
- **Global Version Synchronization**: Synchronized version 1.6.0 across CLI, README, and Web interface.
|
|
355
|
+
- **Dependency Fix**: Ensured `pusher` dependency is correctly included for real-time features.
|
|
356
|
+
|
|
357
|
+
### v1.5.6
|
|
358
|
+
|
|
359
|
+
- **Refined Search History Display**: Improved the search preview logic to use a temporary 'search' view state, providing a smoother experience when opening and canceling search.
|
|
360
|
+
- **Bug Fix**: Resolved an issue where the 'Search Results History' was not displaying correctly in the background.
|
|
361
|
+
|
|
362
|
+
### v1.5.5
|
|
363
|
+
|
|
364
|
+
- **Search Result History**: Automatically saves up to 200 search results.
|
|
365
|
+
- **Enhanced Search UX**: Previously searched items are displayed in the background automatically when opening search.
|
|
366
|
+
- **Deduplication**: Automatically removes duplicate search results to keep history clean.
|
|
367
|
+
|
|
368
|
+
### v1.5.4
|
|
369
|
+
|
|
370
|
+
- **Documentation Refinement**: Clarified installation steps and removed redundant WSL locale guide.
|
|
371
|
+
- **Code Cleanup**: Reverted unnecessary locale settings in source code.
|
|
372
|
+
|
|
373
|
+
### v1.5.3
|
|
374
|
+
|
|
375
|
+
- **Locale Optimization**: Removed complicated locale generation steps for Windows/WSL users. Now relies on standard system locale or simple `C.UTF-8` fallback.
|
|
376
|
+
|
|
377
|
+
### v1.5.2
|
|
378
|
+
|
|
379
|
+
- **Documentation**: Major README overhaul for beginner friendliness. Added dedicated Windows/WSL "Zero-to-Hero" guide.
|
|
380
|
+
|
|
381
|
+
### v1.5.0
|
|
382
|
+
|
|
383
|
+
- **Release**: Milestone v1.5.0 release with polished documentation and stable features.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mytunes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mytunes/app.py,sha256=EBMpGbTzRmynKsNHk1P-5qKH43LZn6KHgRM1SVKKedQ,60855
|
|
3
|
+
mytunes_pro-1.8.0.dist-info/licenses/LICENSE,sha256=lOrP0EIjxcgJia__W3f3PVDZkRd2oRzFkyH2g3LRRCg,1063
|
|
4
|
+
mytunes_pro-1.8.0.dist-info/METADATA,sha256=ihPxSaFC7mLvJfJrIXqY6v55M3h8Qm2yts71GJuBjCg,15294
|
|
5
|
+
mytunes_pro-1.8.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
mytunes_pro-1.8.0.dist-info/entry_points.txt,sha256=6-MsC13nIgzLvrREaGotc32FgxHx_Iuu1z2qCzJs1_4,65
|
|
7
|
+
mytunes_pro-1.8.0.dist-info/top_level.txt,sha256=KWzdFyNNG_sO7GT83-sN5fYArP4_DL5I8HYIwgazXyY,8
|
|
8
|
+
mytunes_pro-1.8.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
mytunes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
mytunes/app.py,sha256=E6-FCofnMY0P7KwxXf-kQCpwFryQvWBubLZKTG_Pj_g,44064
|
|
3
|
-
mytunes_pro-1.5.2.dist-info/licenses/LICENSE,sha256=lOrP0EIjxcgJia__W3f3PVDZkRd2oRzFkyH2g3LRRCg,1063
|
|
4
|
-
mytunes_pro-1.5.2.dist-info/METADATA,sha256=1OjWAq45TTUD3ShYedEGMEEgiZhcKn-lzHQB2KWAWec,12360
|
|
5
|
-
mytunes_pro-1.5.2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
6
|
-
mytunes_pro-1.5.2.dist-info/entry_points.txt,sha256=6-MsC13nIgzLvrREaGotc32FgxHx_Iuu1z2qCzJs1_4,65
|
|
7
|
-
mytunes_pro-1.5.2.dist-info/top_level.txt,sha256=KWzdFyNNG_sO7GT83-sN5fYArP4_DL5I8HYIwgazXyY,8
|
|
8
|
-
mytunes_pro-1.5.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|