-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
molneya
committed
Sep 25, 2024
0 parents
commit f87be9f
Showing
14 changed files
with
675 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
|
||
.pyinstaller/ | ||
.venv/ | ||
__pycache__/ | ||
build/ | ||
data/ | ||
dist/ | ||
icons/ | ||
lists/ | ||
|
||
player.db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
|
||
# unnamed anime song player | ||
|
||
A simple player to play anime songs. | ||
|
||
## Setup | ||
|
||
This guide assumes you're using Windows. If you use something else, good luck. | ||
|
||
### Player | ||
|
||
By default, this music player uses [mpv](https://mpv.io/) to play music. To install (including to PATH), I'd recommend first installing [Chocolatey](https://chocolatey.org/install#individual), then installing [mpv](https://community.chocolatey.org/packages/mpv) using Chocolatey. | ||
|
||
For a list of mpv shortcuts to use during song playback, check the mpv [manual](https://mpv.io/manual/stable/#interactive-control). TL;DR use `0` or `9` to adjust volume, `Space` to pause and `q` to end playback of the current song. | ||
|
||
### Rainmeter | ||
|
||
To help with anime song memorisation, this music player can output the current song to a file, where it can be read by [Rainmeter](https://www.rainmeter.net/) to display it on the screen. | ||
|
||
To use the provided overlay of the current playing song using Rainmeter, you can make a symlink using the command below to add the skin to Rainmeter (requires admin on Windows 10). | ||
``` | ||
cd path/to/this/music/player/folder | ||
mklink /d "%USERPROFILE%\Documents\Rainmeter\Skins\CurrentlyPlaying" "%CD%\skins\CurrentlyPlaying" | ||
``` | ||
|
||
You may also need to refresh the skin to make it show up properly because Rainmeter can be funny sometimes. | ||
|
||
### Lists | ||
|
||
Before playing songs, you need to download a list. Use [AnisongDB](https://anisongdb.com/) to search and download `.json` files. You can place these files into a folder (let's call it `lists`), or into subdirectories for better organisation. | ||
|
||
## Usage | ||
|
||
### Starting the player | ||
|
||
#### Lists | ||
|
||
To play lists (aka those `.json` files) inside the `lists` directory, use the following command: | ||
```player --list lists``` | ||
|
||
Note the above command does not search folders recursively. If you've put files into subdirectories, you need to specify them: | ||
```player --list lists\firstlist lists\someotherlist``` | ||
|
||
The player also understands individual files: | ||
```player --list lists\overlord_ii.json lists\overlord_iv.json``` | ||
|
||
#### Options | ||
|
||
There are many options to adjust how you want the player to behave. You can either make changes to the `options.conf` file, or specify command line arguments. View those command line arguments by using | ||
```player --help``` | ||
|
||
The above lists example can instead be done through the `options.conf` file, for example. | ||
|
||
### Playback | ||
|
||
To go back a song, close the current player (`q` by default) and quickly press `q`. Simply, just double tap the `q` button. | ||
|
||
To end the playlist, close the current player and quickly press `Escape`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
rmdir /s /q dist | ||
:: pyinstaller player.py --onefile --icon icons\icon.ico | ||
pyinstaller player.py --onefile | ||
copy README.md dist\README.md | ||
copy options.conf dist\options.conf | ||
xcopy /s /e /h /I /q skins dist\skins |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
|
||
import logging, sqlite3 | ||
from datetime import datetime | ||
|
||
class Connection: | ||
def __init__(self, path): | ||
self.connect = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES) | ||
|
||
def __enter__(self): | ||
return self.connect.cursor() | ||
|
||
def __exit__(self, exc_type, exc_val, exc_tb): | ||
self.connect.commit() | ||
self.connect.close() | ||
|
||
class Database: | ||
def __init__(self, path): | ||
self.path = path | ||
|
||
def initalise(self): | ||
with Connection(self.path) as con: | ||
con.execute( | ||
""" | ||
CREATE TABLE IF NOT EXISTS songs ( | ||
hash int NOT NULL PRIMARY KEY, | ||
play_count INTEGER NOT NULL, | ||
last_played TIMESTAMP NOT NULL | ||
) | ||
""" | ||
) | ||
|
||
logging.debug("Initialised database") | ||
|
||
def update(self, song): | ||
with Connection(self.path) as cursor: | ||
now = datetime.now() | ||
cursor.execute( | ||
""" | ||
INSERT OR REPLACE INTO songs | ||
VALUES (?, 1, ?) | ||
ON CONFLICT (hash) DO | ||
UPDATE SET | ||
play_count = play_count + 1, | ||
last_played = ? | ||
""", | ||
(hash(song), now, now) | ||
) | ||
|
||
logging.debug(f"Updated database: {hash(song)}") | ||
|
||
def select(self, song): | ||
with Connection(self.path) as cursor: | ||
result = cursor.execute( | ||
""" | ||
SELECT | ||
play_count, | ||
last_played | ||
FROM songs | ||
WHERE hash = ? | ||
""", | ||
(hash(song),) | ||
).fetchone() | ||
|
||
logging.debug(f"Retrieved from database: {hash(song)}: {result}") | ||
|
||
if result: | ||
return result | ||
|
||
return 0, None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
import msvcrt, time | ||
|
||
def getch_or_timeout(timeout): | ||
''' | ||
Waits for `timeout` seconds to check for a user input | ||
''' | ||
time.sleep(timeout) | ||
|
||
if msvcrt.kbhit(): | ||
return msvcrt.getch() | ||
|
||
return |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
|
||
### Values shown below are all defaults. | ||
### If you want to change the option, uncomment the line and change the value after the equals sign. | ||
|
||
# lists to play, can be either directories or files | ||
#lists= | ||
|
||
# the audio player to use | ||
#player=mpv --no-video | ||
|
||
# output file of the currently playing song | ||
#output=skins\CurrentlyPlaying\CurrentlyPlaying.txt | ||
|
||
# show titles in english | ||
#prefer_english=0 | ||
|
||
# use the program without features requiring an internet connection | ||
#offline_mode=0 | ||
|
||
# level of logs to show. Accepted values: DEBUG, INFO, WARNING, ERROR, CRITICAL | ||
#log_level=WARNING | ||
|
||
# difficulty of songs to play | ||
#min_difficulty=0 | ||
#max_difficulty=100 | ||
|
||
# search for things to play | ||
#search_artists= | ||
#search_anime= | ||
|
||
# show results for exact searches only | ||
#exact_search=0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
|
||
import argparse, pathlib, logging, shlex | ||
|
||
class Options: | ||
def __init__(self): | ||
self.lists = [] | ||
self.player = "mpv --no-video" | ||
self.output = r"skins\CurrentlyPlaying\CurrentlyPlaying.txt" | ||
self.prefer_english = False | ||
self.offline_mode = False | ||
self.log_level = "WARNING" | ||
self.min_difficulty = None | ||
self.max_difficulty = None | ||
self.search_artists = [] | ||
self.search_anime = [] | ||
self.exact_search = False | ||
|
||
def from_file(self, file_path): | ||
''' | ||
Sets options from file. This will overwrite the default options. | ||
''' | ||
with open(file_path, 'r') as f: | ||
for line in f.readlines(): | ||
line = line.strip() | ||
|
||
# Skip lines without content or comments | ||
if not line or line.startswith('#'): | ||
continue | ||
|
||
# Unpack key/value pairs, skipping if we get something invalid | ||
try: | ||
key, value = line.split('=', 1) | ||
except ValueError: | ||
continue | ||
|
||
try: | ||
match key.lower(): | ||
# String values | ||
case 'player' | 'output' | 'log_level': | ||
setattr(self, key, value) | ||
# Switches | ||
case 'prefer_english' | 'offline_mode' | 'exact_search': | ||
setattr(self, key, bool(int(value))) | ||
# Floats | ||
case 'min_difficulty' | 'max_difficulty': | ||
setattr(self, key, float(value)) | ||
# Lists | ||
case 'lists' | 'search_artists' | 'search_anime': | ||
setattr(self, key, shlex.split(value)) | ||
# Ignore anything else | ||
case _: | ||
print(f"Ignoring unknown option: '{key}'") | ||
|
||
except (ValueError, AttributeError): | ||
print(f"Ignoring invalid value for option {key}: '{value}'") | ||
|
||
def from_options(self): | ||
''' | ||
Sets options from command line arguments. This will overwrite options loaded from file. | ||
''' | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument("-l", "--list", default=self.lists, type=pathlib.Path, nargs='+', help="lists to play, can be either directories or files") | ||
parser.add_argument("-p", "--player", default=self.player, type=str, help="the audio player to use") | ||
parser.add_argument("-o", "--output", default=self.output, type=pathlib.Path, help="output file of the currently playing song") | ||
parser.add_argument("--prefer-english", default=self.prefer_english, action="store_true", help="show titles in english") | ||
parser.add_argument("--offline-mode", default=self.offline_mode, action="store_true", help="use the program without features requiring an internet connection") | ||
parser.add_argument("--log-level", default=self.log_level, type=str, help="level of logs to show") | ||
parser.add_argument("--min-difficulty", default=self.min_difficulty, type=float, metavar="MIN", help="minimum song difficulty to play") | ||
parser.add_argument("--max-difficulty", default=self.max_difficulty, type=float, metavar="MAX", help="maximum song difficulty to play") | ||
parser.add_argument("--search-artists", default=self.search_artists, type=str, nargs='*', metavar="ARTIST", help="search for artists to play from") | ||
parser.add_argument("--search-anime", default=self.search_anime, type=str, nargs='*', metavar="ANIME", help="search for anime to play from") | ||
parser.add_argument("--exact-search", default=self.exact_search, action="store_true", help="show results for exact searches only") | ||
args = parser.parse_args() | ||
|
||
# Set values | ||
self.lists = args.list | ||
self.player = args.player | ||
self.output = args.output | ||
self.prefer_english = args.prefer_english | ||
self.offline_mode = args.offline_mode | ||
self.log_level = args.log_level | ||
self.min_difficulty = args.min_difficulty | ||
self.max_difficulty = args.max_difficulty | ||
self.search_artists = args.search_artists | ||
self.search_anime = args.search_anime | ||
self.exact_search = args.exact_search |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
|
||
import argparse, logging, os, pathlib | ||
from database import Database | ||
from options import Options | ||
from playlist import Playlist | ||
|
||
def main(): | ||
# Parse player options from file and command line | ||
options = Options() | ||
options.from_file("options.conf") | ||
options.from_options() | ||
|
||
# Create logger | ||
logger = logging.getLogger(__name__) | ||
log_level = getattr(logging, options.log_level, 30) | ||
logging.basicConfig(level=log_level, format="[%(levelname)s] %(message)s") | ||
|
||
# Ensure we have correct directories | ||
os.makedirs("data", exist_ok=True) | ||
|
||
# Set up database | ||
db = Database("player.db") | ||
db.initalise() | ||
|
||
# Start playlist | ||
playlist = Playlist(options, db) | ||
playlist.create() | ||
playlist.play() | ||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# -*- mode: python ; coding: utf-8 -*- | ||
|
||
|
||
a = Analysis( | ||
['player.py'], | ||
pathex=[], | ||
binaries=[], | ||
datas=[], | ||
hiddenimports=[], | ||
hookspath=[], | ||
hooksconfig={}, | ||
runtime_hooks=[], | ||
excludes=[], | ||
noarchive=False, | ||
optimize=0, | ||
) | ||
pyz = PYZ(a.pure) | ||
|
||
exe = EXE( | ||
pyz, | ||
a.scripts, | ||
a.binaries, | ||
a.datas, | ||
[], | ||
name='player', | ||
debug=False, | ||
bootloader_ignore_signals=False, | ||
strip=False, | ||
upx=True, | ||
upx_exclude=[], | ||
runtime_tmpdir=None, | ||
console=True, | ||
disable_windowed_traceback=False, | ||
argv_emulation=False, | ||
target_arch=None, | ||
codesign_identity=None, | ||
entitlements_file=None, | ||
) |
Oops, something went wrong.