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 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.5.2"
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
- "header_help": "[S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [A/5]즐찾추가 [SPC]재생/정지 [Vol]+/- [Q/6]이전",
49
- "help_guide": "[j/k]이동 [En]선택 [h/q]뒤로 [S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [A/5]즐찾추가 [SPC]재생/정지",
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
- "header_help": "[S/1]Search [F/2]Favs [R/3]Hist [M/4]Main [A/5]Add Fav [SPC]Play/Pause [Vol]+/- [Q/6]Back",
77
- "help_guide": "[j/k]Move [En]Select [h/q]Back [S/1]Srch [F/2]Fav [R/3]Hist [M/4]Main [A/5]Add Fav [SPC]P/P",
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() # Clear any buffered keys
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 (3 lines)
929
- self.draw_box(self.stdscr, 0, 0, 3, w, APP_NAME)
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
- help_txt = self.t("header_help")
932
- gap = w - 4 - self.get_display_width(title) - self.get_display_width(help_txt)
933
- if gap < 2: gap = 2
934
- hdr_txt = f"{title}{' '*gap}{help_txt}"
935
- self.stdscr.addstr(1, 2, self.truncate(hdr_txt, w-4), curses.color_pair(1) | curses.A_BOLD)
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 = 3
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.5.2
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.5.2)**
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
- brew install pipx # macOS (설치 경우)
74
+ # pipx install 이후 명령어 등록을 위해 ensurepath 실행 시점 확인!
72
75
  pipx install mytunes-pro
73
- pipx ensurepath # 터미널 명령어 경로 등록 (최초 1회 필수)
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
- `pip install` 이후 실행이 안 된다면, 각 운영체제에 맞는 오디오 엔진(`mpv`)을 설치해 주세요.
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 && sudo apt install mpv
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
- > 윈도우 안에서 리눅스(강력한 개발 도구들)를 마치 일반 앱처럼 쓸 수 있게 해주는 마이크로소프트의 공식 기능입니다. MyTunes는 이 환경에서 리눅스의 강력한 오디오 엔진을 활용해 작동합니다.
118
+ > 윈도우 안에서 리눅스를 앱처럼 쓸 수 있게 해줍니다. MyTunes는 이 환경에서 완벽하게 작동합니다.
114
119
 
115
120
  1. **WSL 설치하기**:
116
- - `시작` 버튼 우클릭 -> `터미널(관리자)` 혹은 `PowerShell(관리자)` 실행.
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
- ```bash
129
- sudo apt update && sudo apt install mpv python3-pip pipx -y
130
- ```
128
+ ```bash
129
+ sudo apt update && sudo apt install mpv python3-pip pipx -y
130
+ ```
131
131
 
132
132
  4. **MyTunes 설치**:
133
- - 이제 위 `빠른 설치` 섹션의 `pipx install` 과정을 그대로 따라하면 끝!
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.5.2)**
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. It uses the **`mpv`** engine for high-quality audio playback.
218
+ **MyTunes Pro** is a Terminal-native application.
222
219
 
223
- - **macOS**: Runs natively in Terminal (iTerm2, Warp recommended).
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)**. See the guide below.
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 # Registers command paths (required once)
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
- If the command fails, please ensure the `mpv` audio engine is installed.
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 && sudo apt install mpv
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
- - Right-click `Start` -> Select `Terminal (Admin)` or `PowerShell (Admin)`.
283
- - Paste this command and hit Enter:
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. **Virtual Environment**:
267
+ 2. **Install Core Tools**:
312
268
  ```bash
313
- python3 -m venv venv
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
- 3. **Run**:
319
- ```bash
320
- python3 mytune.py
321
- ```
322
-
323
- 4. **How to Update**:
272
+ 4. **Install MyTunes**:
324
273
  ```bash
325
- git pull
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
- ### 🧭 Navigation
348
- | Key | Action |
349
- | :--- | :--- |
350
- | `↑` / `↓` / `k` / `j` | Move selection (Vim style supported) |
351
- | `Enter` / `l` | **Select / Play** (Includes `L`) |
352
- | `Space` | Play / Pause |
353
- | `-` / `+` | **Volume** (- / +) |
354
- | `,` / `.` | Seek -10s / +10s |
355
- | `<` / `>` | **Seek -30s / +30s** (Shift) |
356
- | `Backspace` / `h` / `q` | Go back |
357
- | `/` | **Search** (Vim Style) |
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,