litt 0.1.0__tar.gz
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.
- litt-0.1.0/.gitignore +224 -0
- litt-0.1.0/PKG-INFO +60 -0
- litt-0.1.0/README.md +48 -0
- litt-0.1.0/pyproject.toml +23 -0
- litt-0.1.0/requirements.txt +5 -0
- litt-0.1.0/src/litt/__init__.py +0 -0
- litt-0.1.0/src/litt/fonts/NotoSansCJK-Regular.ttc +0 -0
- litt-0.1.0/src/litt/fonts/OFL.txt +92 -0
- litt-0.1.0/src/litt/main.py +251 -0
- litt-0.1.0/src/litt/settings.py +33 -0
litt-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
*.lcov
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
# Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# UV
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
# poetry.lock
|
|
110
|
+
# poetry.toml
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
115
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
116
|
+
# pdm.lock
|
|
117
|
+
# pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# pixi
|
|
122
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
123
|
+
# pixi.lock
|
|
124
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
125
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
126
|
+
.pixi/*
|
|
127
|
+
!.pixi/config.toml
|
|
128
|
+
|
|
129
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
130
|
+
__pypackages__/
|
|
131
|
+
|
|
132
|
+
# Celery stuff
|
|
133
|
+
celerybeat-schedule*
|
|
134
|
+
celerybeat.pid
|
|
135
|
+
|
|
136
|
+
# Redis
|
|
137
|
+
*.rdb
|
|
138
|
+
*.aof
|
|
139
|
+
*.pid
|
|
140
|
+
|
|
141
|
+
# RabbitMQ
|
|
142
|
+
mnesia/
|
|
143
|
+
rabbitmq/
|
|
144
|
+
rabbitmq-data/
|
|
145
|
+
|
|
146
|
+
# ActiveMQ
|
|
147
|
+
activemq-data/
|
|
148
|
+
|
|
149
|
+
# SageMath parsed files
|
|
150
|
+
*.sage.py
|
|
151
|
+
|
|
152
|
+
# Environments
|
|
153
|
+
.env
|
|
154
|
+
.envrc
|
|
155
|
+
.venv
|
|
156
|
+
env/
|
|
157
|
+
venv/
|
|
158
|
+
ENV/
|
|
159
|
+
env.bak/
|
|
160
|
+
venv.bak/
|
|
161
|
+
|
|
162
|
+
# Spyder project settings
|
|
163
|
+
.spyderproject
|
|
164
|
+
.spyproject
|
|
165
|
+
|
|
166
|
+
# Rope project settings
|
|
167
|
+
.ropeproject
|
|
168
|
+
|
|
169
|
+
# mkdocs documentation
|
|
170
|
+
/site
|
|
171
|
+
|
|
172
|
+
# mypy
|
|
173
|
+
.mypy_cache/
|
|
174
|
+
.dmypy.json
|
|
175
|
+
dmypy.json
|
|
176
|
+
|
|
177
|
+
# Pyre type checker
|
|
178
|
+
.pyre/
|
|
179
|
+
|
|
180
|
+
# pytype static type analyzer
|
|
181
|
+
.pytype/
|
|
182
|
+
|
|
183
|
+
# Cython debug symbols
|
|
184
|
+
cython_debug/
|
|
185
|
+
|
|
186
|
+
# PyCharm
|
|
187
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
188
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
189
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
190
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
191
|
+
# .idea/
|
|
192
|
+
|
|
193
|
+
# Abstra
|
|
194
|
+
# Abstra is an AI-powered process automation framework.
|
|
195
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
196
|
+
# Learn more at https://abstra.io/docs
|
|
197
|
+
.abstra/
|
|
198
|
+
|
|
199
|
+
# Visual Studio Code
|
|
200
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
|
|
201
|
+
# can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
202
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer, you
|
|
203
|
+
# could uncomment the following to ignore the entire vscode folder
|
|
204
|
+
# .vscode/
|
|
205
|
+
# Temporary file for partial code execution
|
|
206
|
+
tempCodeRunnerFile.py
|
|
207
|
+
|
|
208
|
+
# Ruff stuff:
|
|
209
|
+
.ruff_cache/
|
|
210
|
+
|
|
211
|
+
# PyPI configuration file
|
|
212
|
+
.pypirc
|
|
213
|
+
|
|
214
|
+
# Marimo
|
|
215
|
+
marimo/_static/
|
|
216
|
+
marimo/_lsp/
|
|
217
|
+
__marimo__/
|
|
218
|
+
|
|
219
|
+
# Streamlit
|
|
220
|
+
.streamlit/secrets.toml
|
|
221
|
+
bin/
|
|
222
|
+
lib64
|
|
223
|
+
pyvenv.cfg
|
|
224
|
+
include/
|
litt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: litt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Display lyrics of playing song in terminal
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: pillow==12.3.0
|
|
7
|
+
Requires-Dist: pyfiglet==1.0.4
|
|
8
|
+
Requires-Dist: rich==15.0.0
|
|
9
|
+
Requires-Dist: simple-term-menu==1.6.6
|
|
10
|
+
Requires-Dist: syncedlyrics==1.0.1
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# LITT
|
|
14
|
+
|
|
15
|
+
LITT (Lyrics in the Terminal) is, as the name suggests, a way to display lyrics of the currently playing song in the terminal.
|
|
16
|
+
Note that this tool currently only supports linux as it uses playerctl but it may be expanded to multiple operating systems in the future.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
Firstly make sure playerctl is installed:
|
|
20
|
+
```bash
|
|
21
|
+
# Debian / Debian-based
|
|
22
|
+
sudo apt install playerctl
|
|
23
|
+
|
|
24
|
+
# Fedora / Fedora-based
|
|
25
|
+
sudo dnf install playerctl
|
|
26
|
+
|
|
27
|
+
# Arch / Arch-based
|
|
28
|
+
sudo pacman -S playerctl
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can run LITT by typing `litt` and then choose your playerctl source.
|
|
32
|
+
To access the settings run `litt --settings`.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
This tool is currently in development and has no official release but you can try out the bleeding edge version by building it yourself with the steps below.
|
|
37
|
+
|
|
38
|
+
## Building
|
|
39
|
+
|
|
40
|
+
Prerequisites
|
|
41
|
+
- python (with pip)
|
|
42
|
+
- git/(gh)
|
|
43
|
+
|
|
44
|
+
1. Clone GitHub repository
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/FabBeyond/litt.git
|
|
47
|
+
cd litt
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. Set up a virtual environment
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv .venv
|
|
53
|
+
source .venv/bin/activate
|
|
54
|
+
pip install -r requirements.txt
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
3. Run the program
|
|
58
|
+
```bash
|
|
59
|
+
python3 src/main.py
|
|
60
|
+
```
|
litt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# LITT
|
|
2
|
+
|
|
3
|
+
LITT (Lyrics in the Terminal) is, as the name suggests, a way to display lyrics of the currently playing song in the terminal.
|
|
4
|
+
Note that this tool currently only supports linux as it uses playerctl but it may be expanded to multiple operating systems in the future.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
Firstly make sure playerctl is installed:
|
|
8
|
+
```bash
|
|
9
|
+
# Debian / Debian-based
|
|
10
|
+
sudo apt install playerctl
|
|
11
|
+
|
|
12
|
+
# Fedora / Fedora-based
|
|
13
|
+
sudo dnf install playerctl
|
|
14
|
+
|
|
15
|
+
# Arch / Arch-based
|
|
16
|
+
sudo pacman -S playerctl
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
You can run LITT by typing `litt` and then choose your playerctl source.
|
|
20
|
+
To access the settings run `litt --settings`.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
This tool is currently in development and has no official release but you can try out the bleeding edge version by building it yourself with the steps below.
|
|
25
|
+
|
|
26
|
+
## Building
|
|
27
|
+
|
|
28
|
+
Prerequisites
|
|
29
|
+
- python (with pip)
|
|
30
|
+
- git/(gh)
|
|
31
|
+
|
|
32
|
+
1. Clone GitHub repository
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/FabBeyond/litt.git
|
|
35
|
+
cd litt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. Set up a virtual environment
|
|
39
|
+
```bash
|
|
40
|
+
python3 -m venv .venv
|
|
41
|
+
source .venv/bin/activate
|
|
42
|
+
pip install -r requirements.txt
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Run the program
|
|
46
|
+
```bash
|
|
47
|
+
python3 src/main.py
|
|
48
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "litt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Display lyrics of playing song in terminal"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"rich==15.0.0",
|
|
13
|
+
"simple-term-menu==1.6.6",
|
|
14
|
+
"pyfiglet==1.0.4",
|
|
15
|
+
"syncedlyrics==1.0.1",
|
|
16
|
+
"pillow==12.3.0"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
litt = "litt.main:main"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/litt"]
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
This Font Software is licensed under the SIL Open Font License,
|
|
2
|
+
Version 1.1.
|
|
3
|
+
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font
|
|
14
|
+
creation efforts of academic and linguistic communities, and to
|
|
15
|
+
provide a free and open framework in which fonts may be shared and
|
|
16
|
+
improved in partnership with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply to
|
|
25
|
+
any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software
|
|
36
|
+
components as distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to,
|
|
39
|
+
deleting, or substituting -- in part or in whole -- any of the
|
|
40
|
+
components of the Original Version, by changing formats or by porting
|
|
41
|
+
the Font Software to a new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION & CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
49
|
+
modify, redistribute, and sell modified and unmodified copies of the
|
|
50
|
+
Font Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components, in
|
|
53
|
+
Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the
|
|
64
|
+
corresponding Copyright Holder. This restriction only applies to the
|
|
65
|
+
primary font name as presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created using
|
|
77
|
+
the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.align import Align
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
5
|
+
from simple_term_menu import TerminalMenu
|
|
6
|
+
import syncedlyrics
|
|
7
|
+
import pyfiglet
|
|
8
|
+
import time
|
|
9
|
+
from requests.exceptions import RequestException
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import argparse
|
|
14
|
+
import re
|
|
15
|
+
import os
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
def cmd(command):
|
|
19
|
+
return subprocess.run(command, capture_output=True, text=True)
|
|
20
|
+
|
|
21
|
+
if subprocess.run(["which", "playerctl"], capture_output=True, text=True).returncode != 0:
|
|
22
|
+
print("Playerctl isnt installed or added to PATH")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
if cmd(["playerctl", "-l"]).stdout.strip() == "":
|
|
25
|
+
print("No playerctl source")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
settings = {}
|
|
29
|
+
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
30
|
+
config_path = os.path.join(config_dir, "litt")
|
|
31
|
+
config_file = os.path.join(config_path, "config.json")
|
|
32
|
+
DEFAULTS = {"font": "base", "font_style": "blocky"}
|
|
33
|
+
os.makedirs(config_path, exist_ok=True)
|
|
34
|
+
if not os.path.isfile(config_file):
|
|
35
|
+
with open(config_file, "w") as f:
|
|
36
|
+
json.dump(DEFAULTS, f, indent=2)
|
|
37
|
+
with open(config_file) as f:
|
|
38
|
+
settings = {**DEFAULTS, **json.load(f)}
|
|
39
|
+
|
|
40
|
+
parser = argparse.ArgumentParser("LITT")
|
|
41
|
+
parser.add_argument("--settings", action="store_true")
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
if args.settings:
|
|
44
|
+
from litt import settings
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
TOKEN_RE = re.compile(
|
|
48
|
+
r'[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f\uac00-\ud7a3]'
|
|
49
|
+
r'|\s+'
|
|
50
|
+
r'|\S+'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
console = Console()
|
|
54
|
+
players = subprocess.run(["playerctl", "-l"], capture_output=True, text=True).stdout.strip()
|
|
55
|
+
players = players.split("\n")
|
|
56
|
+
player_menu = TerminalMenu(players, title="Choose your playerctl source:")
|
|
57
|
+
selected_player = player_menu.show()
|
|
58
|
+
if selected_player is None:
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
selected_player = players[selected_player]
|
|
61
|
+
|
|
62
|
+
POLL_RATE = 0.1
|
|
63
|
+
|
|
64
|
+
def parse_timestamp(stamp):
|
|
65
|
+
try:
|
|
66
|
+
stamp = stamp.split("]")[0][1:].replace(".", ":")
|
|
67
|
+
dt = datetime.strptime(stamp, "%M:%S:%f")
|
|
68
|
+
return dt.minute * 60 + dt.second + dt.microsecond / 1000000
|
|
69
|
+
except:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def get_position():
|
|
73
|
+
result = playerctl("position")
|
|
74
|
+
return float(result.stdout.strip())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def playerctl(*args):
|
|
78
|
+
return cmd(["playerctl", "-p", selected_player, *args])
|
|
79
|
+
|
|
80
|
+
def get_playing_song():
|
|
81
|
+
return playerctl("metadata", "--format", "{{title}} -||- {{artist}}").stdout.strip().split(" -||- ")
|
|
82
|
+
|
|
83
|
+
def wait_until_next_song():
|
|
84
|
+
last_song = get_playing_song()
|
|
85
|
+
while get_playing_song() == last_song:
|
|
86
|
+
last_song = get_playing_song()
|
|
87
|
+
time.sleep(1)
|
|
88
|
+
def blockify(text, consoleWidth, font_path="", size=25):
|
|
89
|
+
if settings["font"] == "base":
|
|
90
|
+
font_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fonts", "NotoSansCJK-Regular.ttc")
|
|
91
|
+
else:
|
|
92
|
+
font_path = settings["font"]
|
|
93
|
+
|
|
94
|
+
font = ImageFont.truetype(font_path, size)
|
|
95
|
+
wrapped = wrap_text(text, font, consoleWidth * (1 if settings["font_style"] == "blocky" else 2))
|
|
96
|
+
|
|
97
|
+
measure = ImageDraw.Draw(Image.new("L", (1, 1)))
|
|
98
|
+
l, t, r, b = measure.multiline_textbbox((0, 0), wrapped, font=font)
|
|
99
|
+
|
|
100
|
+
lines = []
|
|
101
|
+
if settings["font_style"] == "blocky":
|
|
102
|
+
lines = blocky_lyrics(l, t, r, b, wrapped, font)
|
|
103
|
+
elif settings["font_style"] == "braille":
|
|
104
|
+
lines = braille_lyrics(l, t, r, b, wrapped, font)
|
|
105
|
+
|
|
106
|
+
return Text("\n".join(lines))
|
|
107
|
+
|
|
108
|
+
def blocky_lyrics(l, t, r, b, wrapped, font):
|
|
109
|
+
width = r - l
|
|
110
|
+
height = b - t
|
|
111
|
+
height += height % 2
|
|
112
|
+
|
|
113
|
+
img = Image.new("L", (width, height), 0)
|
|
114
|
+
ImageDraw.Draw(img).multiline_text((-l, -t), wrapped, align="center", font=font, fill=255)
|
|
115
|
+
px = img.load()
|
|
116
|
+
|
|
117
|
+
lines = []
|
|
118
|
+
for y in range(0, height, 2):
|
|
119
|
+
line = ""
|
|
120
|
+
for x in range(width):
|
|
121
|
+
top_filled = px[x, y] > 0
|
|
122
|
+
bottom_filled = px[x, y+1] > 0
|
|
123
|
+
|
|
124
|
+
if top_filled and bottom_filled:
|
|
125
|
+
line += "█"
|
|
126
|
+
elif top_filled:
|
|
127
|
+
line += "▀"
|
|
128
|
+
elif bottom_filled:
|
|
129
|
+
line += "▄"
|
|
130
|
+
else:
|
|
131
|
+
line += " "
|
|
132
|
+
lines.append(line)
|
|
133
|
+
|
|
134
|
+
return lines
|
|
135
|
+
|
|
136
|
+
def braille_lyrics(l, t, r, b, wrapped, font):
|
|
137
|
+
BRAILLE_BITS = {
|
|
138
|
+
(0, 0): 0x01, (0, 1): 0x02, (0, 2): 0x04, (0, 3): 0x40,
|
|
139
|
+
(1, 0): 0x08, (1, 1): 0x10, (1, 2): 0x20, (1, 3): 0x80,
|
|
140
|
+
}
|
|
141
|
+
width = r - l
|
|
142
|
+
width += width % 2
|
|
143
|
+
height = b - t
|
|
144
|
+
height += (4 - height % 4) % 4
|
|
145
|
+
|
|
146
|
+
img = Image.new("L", (width, height), 0)
|
|
147
|
+
ImageDraw.Draw(img).multiline_text((-l, -t), wrapped, align="center", font=font, fill=255)
|
|
148
|
+
px = img.load()
|
|
149
|
+
|
|
150
|
+
lines = []
|
|
151
|
+
for y in range(0, height, 4):
|
|
152
|
+
line = ""
|
|
153
|
+
for x in range(0, width, 2):
|
|
154
|
+
bits = 0
|
|
155
|
+
for (dx, dy), bit in BRAILLE_BITS.items():
|
|
156
|
+
px_x = x + dx
|
|
157
|
+
py_y = y + dy
|
|
158
|
+
if px_x < width and py_y < height and px[px_x, py_y] > 128:
|
|
159
|
+
bits |= bit
|
|
160
|
+
line += chr(0x2800 + bits)
|
|
161
|
+
lines.append(line)
|
|
162
|
+
|
|
163
|
+
return lines
|
|
164
|
+
|
|
165
|
+
def wrap_text(text, font, max_width):
|
|
166
|
+
tokens = TOKEN_RE.findall(text)
|
|
167
|
+
lines = []
|
|
168
|
+
current = ""
|
|
169
|
+
|
|
170
|
+
for token in tokens:
|
|
171
|
+
candidate = f"{current} {token}".strip()
|
|
172
|
+
if font.getlength(candidate) <= max_width or not current:
|
|
173
|
+
current = candidate
|
|
174
|
+
else:
|
|
175
|
+
lines.append(current)
|
|
176
|
+
current = token
|
|
177
|
+
if current:
|
|
178
|
+
lines.append(current)
|
|
179
|
+
return "\n".join(lines)
|
|
180
|
+
|
|
181
|
+
def main():
|
|
182
|
+
while True:
|
|
183
|
+
console.clear()
|
|
184
|
+
try:
|
|
185
|
+
playing_song, artist = get_playing_song()
|
|
186
|
+
lyrics = syncedlyrics.search(f"{playing_song} {artist}")
|
|
187
|
+
except RequestException as e:
|
|
188
|
+
print(f"Network error {e}")
|
|
189
|
+
wait_until_next_song()
|
|
190
|
+
continue
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f"An unexpected error occured {e}")
|
|
193
|
+
wait_until_next_song()
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if lyrics is None:
|
|
197
|
+
print("no lyrics :(")
|
|
198
|
+
wait_until_next_song()
|
|
199
|
+
continue
|
|
200
|
+
if lyrics == []:
|
|
201
|
+
print("no lyrics :(")
|
|
202
|
+
wait_until_next_song()
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
lyrics = lyrics.split("\n")
|
|
206
|
+
idx = 0
|
|
207
|
+
last_song = get_playing_song()
|
|
208
|
+
global_offset = list(filter(lambda x: "offset" in x.lower(), lyrics))
|
|
209
|
+
if len(global_offset) == 0:
|
|
210
|
+
global_offset = 0
|
|
211
|
+
else:
|
|
212
|
+
global_offset = list(global_offset)[0]
|
|
213
|
+
global_offset = float(global_offset.split(":")[1].strip()[:-1])
|
|
214
|
+
|
|
215
|
+
timed_lyrics = []
|
|
216
|
+
for lyric in lyrics:
|
|
217
|
+
timestamp = parse_timestamp(lyric)
|
|
218
|
+
if timestamp is None:
|
|
219
|
+
continue
|
|
220
|
+
timestamp += global_offset
|
|
221
|
+
|
|
222
|
+
timed_lyrics.append((timestamp, lyric.split("]")[1].strip()))
|
|
223
|
+
|
|
224
|
+
last_song = get_playing_song()
|
|
225
|
+
|
|
226
|
+
while True:
|
|
227
|
+
pos = get_position()
|
|
228
|
+
text = list(filter(lambda x: x[0] <= pos+global_offset, timed_lyrics))
|
|
229
|
+
if len(text) == 0:
|
|
230
|
+
continue
|
|
231
|
+
content = text[len(text)-1][1].strip()
|
|
232
|
+
|
|
233
|
+
new_song = get_playing_song()
|
|
234
|
+
if new_song != last_song:
|
|
235
|
+
break
|
|
236
|
+
last_song = new_song
|
|
237
|
+
|
|
238
|
+
banner = Text(content, justify="center")
|
|
239
|
+
|
|
240
|
+
text = Align(
|
|
241
|
+
blockify(content, console.width),
|
|
242
|
+
vertical="middle",
|
|
243
|
+
align="center",
|
|
244
|
+
height=console.height
|
|
245
|
+
)
|
|
246
|
+
console.print(text)
|
|
247
|
+
|
|
248
|
+
time.sleep(POLL_RATE)
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from simple_term_menu import TerminalMenu
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
6
|
+
config_file = os.path.join(config_dir, "litt", "config.json")
|
|
7
|
+
settings = json.loads(open(config_file, "r").read())
|
|
8
|
+
|
|
9
|
+
font_options = ["Style", "Font"]
|
|
10
|
+
font_menu = TerminalMenu(font_options, title="Settings")
|
|
11
|
+
answer = font_menu.show()
|
|
12
|
+
|
|
13
|
+
if answer == 0:
|
|
14
|
+
style_options = ["Blocky", "Braille"]
|
|
15
|
+
style_menu = TerminalMenu(style_options, title="Font Style")
|
|
16
|
+
answer = style_menu.show()
|
|
17
|
+
|
|
18
|
+
if answer == 0:
|
|
19
|
+
settings["font_style"] = "blocky"
|
|
20
|
+
elif answer == 1:
|
|
21
|
+
settings["font_style"] = "braille"
|
|
22
|
+
|
|
23
|
+
elif answer == 1:
|
|
24
|
+
fontfont_options = ["Base", "Custom"]
|
|
25
|
+
fontfont_menu = TerminalMenu(fontfont_options, title="Font")
|
|
26
|
+
answer = fontfont_menu.show()
|
|
27
|
+
|
|
28
|
+
if answer == 0:
|
|
29
|
+
settings["font"] = "base"
|
|
30
|
+
elif answer == 1:
|
|
31
|
+
settings["font"] = input("Custom Font Path: ")
|
|
32
|
+
|
|
33
|
+
open(config_file, "w").write(json.dumps(settings, indent=2))
|