Skip to content

Commit f2e5a1f

Browse files
committed
Switch to simple terminal UI; auto-launch VLC
Replace the textual TUI with a lightweight terminal UI and add auto-launch for VLC. Main changes: start VLC process on supported platforms, run the fetch loop via asyncio.run, and use src.front.simpleTerm.fetchThose to render status/lyrics in the console. Add lyrics support in fetching, introduce src/util/print for colored logging, and harden LastFM calls (artist sanitization and exception logging). Remove config.json from the repo and add it to .gitignore, update requirements and build.spec (remove some hiddenimports and adjust deps), and add a preview image. Deleted the old textual TUI (src/front/tui.py) and replaced it with src/front/simpleTerm.py. And also, `README.md` changes. (Written By Co-Pilot)
1 parent 913f74c commit f2e5a1f

12 files changed

Lines changed: 237 additions & 130 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ __pycache__
55
*.pyc
66

77
build
8-
dist
8+
dist
9+
10+
desktop.ini
11+
config.json
12+
VLC.UTILS.CONFIG.json

README.md

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,24 @@
77
<img src="https://img.shields.io/badge/Made%20with-Python-3776AB?style=flat-square&logo=python&logoColor=white"/>
88
<!-- <img src="https://img.shields.io/badge/Intergraded%20with-Last.fm-D51007?style=flat-square&logo=last.fm&logoColor=white"/>
99
<img src="https://img.shields.io/badge/Intergraded%20with-Discord%20RPC-5663f7?style=flat-square&logo=discord&logoColor=white"/> -->
10+
11+
<img src="readme/Preview.png" alt="preview"/>
1012
</div>
1113

1214
## Features
13-
- **Discord RPC**: Shows what you're listening to with sync timers and high-quality album art.
15+
- **Discord RPC**: Shows what you're listening to with sync timed and album art.
1416
- **Last.fm**: Automatic "Now Playing" updates and threshold-based scrobbling.
15-
- **TUI**: A beautiful terminal interface with a live progress bar and album info.
1617
- **Cross-Platform**: Ready to be compiled for Windows, macOS, and Linux.
1718

1819
## Setup
19-
1. **VLC Configuration**:
20-
- Open VLC -> Tools -> Preferences -> All.
21-
- Interface -> Main interfaces -> Check "Web".
22-
- Interface -> Main interfaces -> Lua -> Set a **Lua Password** (e.g., `helloworld`).
23-
2. **Settings**:
20+
1. **Settings**:
2421
- Create a config.json in the root folder (see config.json template).
2522
- Enter your VLC password, Discord Application Client ID (Not your UserID), and Last.fm API credentials.
23+
2. **Opening the app**:
24+
- Just open the tool and that's it.
2625

27-
## Running the software
28-
### Running the software from source
26+
## Running the tool
27+
### Running the tool from source
2928
1. Make sure you have [Python](https://python.org/) installed.
3029
2. Clone the repository and navigate into it.
3130
3. Open the `config.json` and configure the file.
@@ -34,7 +33,7 @@
3433
6. Install dependencies: `pip install -r requirements.txt`
3534
7. Run: `python main.py`
3635

37-
### Compile the software from source
36+
### Compile the tool from source
3837
1. Make sure you have [Python](https://python.org/) installed.
3938
2. Clone the repository and navigate into it.
4039
3. Open Powershell (Reccomended) or Command Prompt.
@@ -43,17 +42,17 @@
4342
6. Run: `pyinstaller build.spec`
4443
7. Navigate to `dist/`
4544
8. Create the `config.json` and configure the file.
46-
9. Open the software
45+
9. Open the tool
4746

48-
### Download the software from nightly.link
47+
### Download the tool from nightly.link
4948
1. Download the one that's compatible with your operating system
50-
- Windows: https://nightly.link/daveberrys/VLC.UTILS/workflows/building/main/VLC.UTILS-Windows.zip
51-
- macOS: https://nightly.link/daveberrys/VLC.UTILS/workflows/building/main/VLC.UTILS-MacOS.zip
52-
- Linux: https://nightly.link/daveberrys/VLC.UTILS/workflows/building/main/VLC.UTILS-Linux.zip
49+
- Windows: https://nightly.link/daveberrys/VLC.UTILS/workflows/building/main/VLC.UTILS-Windows.zip
50+
- macOS: https://nightly.link/daveberrys/VLC.UTILS/workflows/building/main/VLC.UTILS-MacOS.zip
51+
- Linux: https://nightly.link/daveberrys/VLC.UTILS/workflows/building/main/VLC.UTILS-Linux.zip
5352
2. Create a new folder
5453
3. Move `VLC.UTILS` to the new folder
5554
4. Create the `config.json` and configure the file.
56-
5. Open the software
55+
5. Open the tool
5756

5857
> [!CAUTION]
59-
> Some of the code is made by AI (Gemini 3.0 Flash). But I've taken quiz from them and checked the code and everything is fine. Don't worry about your shit being stolen. The reason why I use LLMs is because of two things. ADHD and short-term memory loss. I struggle with trying to remember things and I need the usage of LLM to help me out on giving me quizzes so I remember and I re-learn it again.
58+
> Some of the code is made by AI (Gemini 3.0 Flash). But I've taken quiz from them and checked the code and everything is fine. Don't worry about your shit being stolen. The reason why I use LLMs is because of two things. ADHD and short-term memory loss. I struggle with trying to remember things and I need the usage of LLM to help me out on giving me quizzes so I remember and I re-learn it again.

build.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import os
44
from PyInstaller.utils.hooks import collect_all
55

66
datas, binaries, existing_hiddenimports = collect_all('textual')
7-
hiddenimports = existing_hiddenimports + ['pypresence', 'pylast', 'requests', 'termcolor', 'rich', 'rich._unicode_data.unicode17-0-0']
7+
hiddenimports = existing_hiddenimports + ['pypresence', 'pylast', 'requests', 'termcolor']
88

99
block_cipher = None
1010

config.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

main.py

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,54 @@
11
import asyncio
2-
import time
32
import json
4-
from src.fetching import getVlcStatus
3+
import os
4+
import subprocess
5+
import time
6+
from sys import platform
7+
58
from src.discordrpc import DiscordRPC
9+
from src.fetching import getVlcStatus
10+
from src.front.simpleTerm import fetchThose
611
from src.lastfm import LastFM
7-
from src.front.tui import VlcTui
12+
import src.util.print as print
813

9-
class VlcApp(VlcTui):
14+
class VlcApp:
1015
def __init__(self, config):
11-
super().__init__()
1216
self.config = config
1317
self.lastTrackID = None
1418
self.currentArt = "vlc"
1519
self.scrobbled = False
1620
self.isShowing = False
21+
self.vlc_process = None
22+
23+
def startup(self):
24+
path = ""
25+
if platform == "win32":
26+
if os.path.exists("C:\\Program Files\\VideoLAN\\VLC\\vlc.exe"):
27+
path = "C:\\Program Files\\VideoLAN\\VLC\\vlc.exe"
28+
elif os.path.exists("C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe"):
29+
path = "C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe"
30+
elif platform == "darwin":
31+
if os.path.exists("/Applications/VLC.app/Contents/MacOS/VLC"):
32+
path = "/Applications/VLC.app/Contents/MacOS/VLC"
33+
elif os.path.exists("/Applications/VLC.app"):
34+
path = "/Applications/VLC.app"
35+
elif platform == "linux":
36+
if os.path.exists("/usr/bin/vlc"):
37+
path = "/usr/bin/vlc"
38+
elif os.path.exists(
39+
"/var/lib/flatpak/app/org.videolan.VLC/x86_64/stable/active/export/bin/org.videolan.VLC"
40+
):
41+
path = "/var/lib/flatpak/app/org.videolan.VLC/x86_64/stable/active/export/bin/org.videolan.VLC"
42+
elif os.path.exists("/snap/bin/vlc"):
43+
path = "/snap/bin/vlc"
1744

18-
async def on_mount(self):
45+
if path:
46+
self.vlc_process = subprocess.Popen([path, "--extraintf=http"])
47+
else:
48+
print("VLC not found or unsupported platform.")
49+
50+
async def run(self):
51+
self.startup()
1952
self.rpc = DiscordRPC(self.config["discordID"])
2053
self.lfm = LastFM(
2154
self.config["lfmKey"],
@@ -24,13 +57,35 @@ async def on_mount(self):
2457
self.config["lfmPass"],
2558
)
2659
await self.rpc.connect()
27-
self.run_worker(self.fetcherLoop())
60+
await self.fetcherLoop()
2861

2962
async def fetcherLoop(self):
3063
await asyncio.sleep(0.1)
64+
3165
while True:
66+
if self.vlc_process and self.vlc_process.poll() is not None:
67+
print("VLC closed. Exiting...")
68+
await self.rpc.clear()
69+
return
70+
3271
status = getVlcStatus(self.config["vlcPassword"], self.config["vlcPort"])
3372

73+
if isinstance(status, dict):
74+
fetchThose(
75+
status["title"],
76+
status["artist"],
77+
status["album"],
78+
status["state"],
79+
status["position"],
80+
status["length"],
81+
self.config["refreshRate"],
82+
self.scrobbled,
83+
status["lyrics"],
84+
)
85+
else:
86+
os.system("cls" if os.name == "nt" else "clear")
87+
print(status)
88+
3489
if isinstance(status, dict) and status["state"] == "playing":
3590
trackID = f"{status['title']} - {status['artist']}"
3691

@@ -46,13 +101,6 @@ async def fetcherLoop(self):
46101
self.scrobbled = False
47102
self.isShowing = True
48103

49-
elif not self.isShowing:
50-
self.lfm.updateNowPlaying(
51-
status["artist"], status["title"], status["album"]
52-
)
53-
await self.rpc.updateStatus(status, self.currentArt)
54-
self.isShowing = True
55-
56104
if not self.scrobbled and status["length"] > 30:
57105
threshold = min(status["length"] / 2, 240)
58106
if status["position"] >= threshold:
@@ -62,25 +110,31 @@ async def fetcherLoop(self):
62110
status["album"],
63111
int(time.time()),
64112
)
65-
self.lfm.updateNowPlaying(
66-
status["artist"], status["title"], status["album"]
67-
)
68113
self.scrobbled = True
69-
70-
self.updateInfo(status, self.currentArt)
71-
72-
elif isinstance(status, dict) and status["state"] != "playing":
114+
else:
73115
if self.isShowing:
74116
await self.rpc.clear()
75117
self.isShowing = False
76-
self.updateInfo(status, self.currentArt)
118+
self.scrobbled = False
77119

78120
await asyncio.sleep(self.config["refreshRate"])
79121

80122

123+
def loadConfig():
124+
if os.path.exists("config.json"):
125+
with open("config.json", "r") as f:
126+
config = json.load(f)
127+
elif os.path.exists("VLC.UTILS.CONFIG.json"):
128+
with open("VLC.UTILS.CONFIG.json", "r") as f:
129+
config = json.load(f)
130+
else:
131+
print.error("Config not found.")
132+
exit()
133+
return config
134+
135+
81136
if __name__ == "__main__":
82-
with open("config.json", "r") as f:
83-
config = json.load(f)
137+
config = loadConfig()
84138

85139
app = VlcApp(config)
86-
app.run()
140+
asyncio.run(app.run())

readme/Preview.png

13.4 KB
Loading

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ pypresence
22
pylast
33
requests
44
termcolor
5-
textual
65
pyinstaller

src/fetching.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def getVlcStatus(password, port, host="localhost"):
1212
"title": meta.get('title', 'Unknown Title'),
1313
"artist": meta.get('artist', 'Unknown Artist'),
1414
"album": meta.get('album', 'Unknown Album'),
15+
"lyrics": meta.get('LYRICS', 'Unknown Lyrics'),
1516
"position": data.get('time', 0),
1617
"length": data.get('length', 0),
1718
"state": data.get('state', 'stopped')

src/front/simpleTerm.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import os
2+
import re
3+
from time import sleep
4+
5+
6+
def formatTime(seconds):
7+
mins, secs = divmod(seconds, 60)
8+
return f"{mins:02}:{secs:02}"
9+
10+
11+
def stateThis(state):
12+
stateUse = ""
13+
if state == "playing":
14+
stateUse = ">"
15+
elif state == "paused":
16+
stateUse = "||"
17+
else:
18+
stateUse = "?"
19+
return stateUse
20+
21+
22+
def lyricsThis(lyrics, position):
23+
outputLines = []
24+
if lyrics and lyrics != "Unknown Lyrics":
25+
lyrics_lines = lyrics.strip().split("\n")
26+
timedLyrics = []
27+
time_pattern = re.compile(r"\[(\d{2}):(\d{2})(?:\.\d{2})?\]\s*(.*)")
28+
29+
for line in lyrics_lines:
30+
match = time_pattern.match(line)
31+
if match:
32+
minutes = int(match.group(1))
33+
seconds = int(match.group(2))
34+
text = match.group(3)
35+
timedLyrics.append({"time": minutes * 60 + seconds, "text": text})
36+
37+
if timedLyrics:
38+
currentLyricText = "..."
39+
for i in range(len(timedLyrics)):
40+
if timedLyrics[i]["time"] <= position:
41+
currentLyricText = timedLyrics[i]["text"]
42+
if (
43+
i + 1 < len(timedLyrics)
44+
and position < timedLyrics[i + 1]["time"]
45+
):
46+
break
47+
elif timedLyrics[i]["time"] > position:
48+
break
49+
outputLines.append(f"{currentLyricText}")
50+
else:
51+
outputLines.extend(lyrics_lines)
52+
else:
53+
outputLines.append("No lyrics available.")
54+
return "\n".join(outputLines)
55+
56+
57+
def fetchThose(
58+
song,
59+
artist,
60+
album,
61+
state,
62+
position,
63+
length,
64+
refreshRate,
65+
scrobbled,
66+
lyrics,
67+
):
68+
os.system("cls" if os.name == "nt" else "clear")
69+
70+
print("~~~~~~~~~~~~~~~~~~~ VLC.UTILS ~~~~~~~~~~~~~~~~~~~")
71+
print(f"Song: {song}")
72+
print(f"Artist: {artist}")
73+
print(f"Album: {album}")
74+
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
75+
print(f"{stateThis(state)} - {formatTime(position)} / {formatTime(length)}")
76+
print("~~~~~~~~~~~~~~~~~~~~ LAST.FM ~~~~~~~~~~~~~~~~~~~~")
77+
print(f"Scrobbled: {scrobbled}")
78+
print("~~~~~~~~~~~~~~~~~~~~ LYRIC ~~~~~~~~~~~~~~~~~~~~~")
79+
print(lyricsThis(lyrics, position))
80+
81+
sleep(refreshRate)

0 commit comments

Comments
 (0)