diff --git a/README.md b/README.md index 8e91009..5300725 100644 --- a/README.md +++ b/README.md @@ -8,145 +8,155 @@ Rosé is a music manager for Unix-based systems. Rosé provides a virtual FUSE filesystem for managing your music library and various functions for editing and improving your music library's metadata and tags. -> [!NOTE] -> Rosé modifies the managed audio files. If you do not want to modify your -> audio files, for example because they are seeding in a bittorrent client, you -> should not use Rosé. - -TODO: Video +Rosé's core functionality is taking in a directory of music and creating a +virtual filesystem based on the music's tags. -## Installation - -Install Rosé with Nix Flakes. If you do not have Nix Flakes, you can install it -with [this installer](https://github.com/DeterminateSystems/nix-installer). +So for example, given the following directory of music files: -```bash -$ nix profile install github:azuline/rose#rose +``` +source/ +├── !collages +│   └── Road Trip.toml +├── !playlists +│   └── Shower.toml +├── BLACKPINK - 2016. SQUARE ONE +│   ├── 01. WHISTLE.opus +│   ├── 02. BOOMBAYAH.opus +│   └── cover.jpg +├── BLACKPINK - 2016. SQUARE TWO +│   ├── 01. PLAYING WITH FIRE.opus +│   ├── 02. STAY.opus +│   ├── 03. WHISTLE (acoustic ver.).opus +│   └── cover.jpg +├── LOOΠΔ - 2017. Kim Lip +│   ├── 01. Eclipse.opus +│   ├── 02. Twilight.opus +│   └── cover.jpg +├── LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match +│   ├── 01. ODD.opus +│   ├── 02. Girl Front.opus +│   ├── 03. LOONATIC.opus +│   ├── 04. Chaotic.opus +│   ├── 05. Starlight.opus +│  └── cover.jpg +└── YUZION - 2019. Young Trapper + ├── 01. Look At Me!!.mp3 + ├── 02. In My Pocket.mp3 + ├── 03. Henzclub.mp3 + ├── 04. Ballin'.mp3 + ├── 05. Jealousy.mp3 + ├── 06. 18.mp3 + ├── 07. Still Love.mp3 + ├── 08. Next Up.mp3 + └── cover.jpg ``` -In the future, other packaging systems may be considered. However, I strongly -dislike Python's packaging story, hence: Nix. - -## Quickstart - -TODO - -## Features - -TODO - -## Requirements - -TODO - -## License - -Copyright 2023 blissful +Rosé produces the following virtual filesystem (duplicate information has been +omitted). -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0. +``` +virtual/ +├── 1. Releases/ +│   ├── {NEW} LOOΠΔ - 2017. Kim Lip - Single [K-Pop]/ +│   │   ├── 01. LOOΠΔ - Eclipse.opus +│   │   ├── 02. LOOΠΔ - Twilight.opus +│   │   └── cover.jpg +│   ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment}/ +│   │   └── ... +│   ├── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment}/ +│   │   └── ... +│   ├── LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop] {BlockBerry Creative}/ +│   │   └── ... +│   └── YUZION - 2019. Young Trapper [Hip Hop]/ +│   └── ... +├── 2. Releases - New/ +│   └── [2023-10-25] {NEW} LOOΠΔ - 2017. Kim Lip - Single [K-Pop]/... +├── 3. Releases - Recently Added/ +│   ├── [2023-10-25] {NEW} LOOΠΔ - 2017. Kim Lip - Single [K-Pop]/... +│   ├── [2023-10-01] LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop] {BlockBerry Creative}/... +│   ├── [2022-08-22] BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment}/... +│   ├── [2022-08-10] BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment}/... +│   └── [2019-09-16] YUZION - 2019. Young Trapper [Hip Hop]/... +├── 4. Artists/ +│   ├── BLACKPINK/ +│   │   ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment}/... +│   │   └── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment}/... +│   ├── LOOΠΔ/ +│   │   ├── {NEW} LOOΠΔ - 2017. Kim Lip - Single [K-Pop] +│   │   └── LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop] {BlockBerry Creative}/... +│   ├── LOOΠΔ ODD EYE CIRCLE/ +│   │   └── LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop] {BlockBerry Creative}/... +│   └── YUZION/ +│   └── YUZION - 2019. Young Trapper [Hip Hop]/... +├── 5. Genres/ +│   ├── Hip Hop/ +│   │   └── YUZION - 2019. Young Trapper [Hip Hop]/... +│   └── K-Pop/ +│   ├── {NEW} LOOΠΔ - 2017. Kim Lip - Single [K-Pop]/... +│   ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment}/... +│   ├── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment}/... +│   └── LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop] {BlockBerry Creative}/... +├── 6. Labels/ +│   ├── BlockBerry Creative/ +│   │   ├── {NEW} LOOΠΔ - 2017. Kim Lip - Single [K-Pop]/... +│   │ └── LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop] {BlockBerry Creative}/... +│   └── YG Entertainment/ +│   ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment}/... +│   └── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment}/... +├── 7. Collages/ +│   └── Road Trip/ +│   ├── 1. BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment}/... +│   └── 2. LOOΠΔ ODD EYE CIRCLE - 2017. Mix & Match - EP [K-Pop] {BlockBerry Creative}/... +└── 8. Playlists/ + └── Shower/ + ├── 1. LOOΠΔ ODD EYE CIRCLE - Chaotic.opus + ├── 2. YUZION - Jealousy.mp3 + ├── 3. BLACKPINK - PLAYING WITH FIRE.opus + └── 4. LOOΠΔ - Eclipse.opus +``` -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. +Rosé's virtual filesystem organizes your music library by the metadata in the +music tags. In addition to a flat directory of all releases, Rosé creates +additional directories based on Date Added, Artist, Genre, and Label. -## Contributions +Rosé also provides support for creating Collages (collections of releases) and +Playlists (collections of tracks). These are configured as TOML files in the +source directory. -TODO +Because the quality of the virtual filesystem depends on the quality of the +tags, Rosé also provides functions for improving the tags of your music +library. Rosé provides an easy text-based interface for manually modifying +metadata, automatic metadata importing from third-party sources, and a rules +system to automatically apply metadata changes based on patterns. -# OLD +> [!NOTE] +> Rosé modifies the managed audio files, even on first scan. If you do not want +> to modify your audio files, for example because they are seeding in a +> bittorrent client, you should not use Rosé. -## The Virtual Filesystem +_Demo Video TBD_ -Rosé reads a source directory of releases like this: +## Installation -```tree -. -├── BLACKPINK - 2016. SQUARE ONE -├── BLACKPINK - 2016. SQUARE TWO -├── LOOΠΔ - 2019. [X X] -├── LOOΠΔ - 2020. [#] -├── LOOΠΔ 1_3 - 2017. Love & Evil -├── LOOΠΔ ODD EYE CIRCLE - 2017. Max & Match -└── YUZION - 2019. Young Trapper -``` +Install Rosé with Nix Flakes. If you do not have Nix Flakes, you can install it +with [this installer](https://github.com/DeterminateSystems/nix-installer). -And constructs a virtual filesystem from the source directory's audio tags. The -virtual filesystem enables viewing various subcollections of the source -directory based on multiple types of tags as a filesystem. - -While music players and music servers enable viewing releases with similar -filters, those filters are only available in a proprietary UI. Rosé provides -this filtering as a filesystem, which is easily composable with other tools and -systems. - -The virtual filesystem constructed from the above source directory is: - -```tree -. -├── Releases -│   ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment} -│   ├── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment} -│   ├── LOOΠΔ 1_3 - 2017. Love & Evil [K-Pop] {BlockBerry Creative} -│   ├── LOOΠΔ - 2019. [X X] [K-Pop] {BlockBerry Creative} -│   ├── LOOΠΔ - 2020. [#] [K-Pop] {BlockBerry Creative} -│   ├── LOOΠΔ ODD EYE CIRCLE - 2017. Max & Match [K-Pop] {BlockBerry Creative} -│   └── YUZION - 2019. Young Trapper [Hip Hop] {No Label} -├── Artists -│   ├── BLACKPINK -│   │   ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment} -│   │ └── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment} -│   ├── LOOΠΔ -│   │   ├── LOOΠΔ - 2019. [X X] [K-Pop] {BlockBerry Creative} -│   │ └── LOOΠΔ - 2020. [#] [K-Pop] {BlockBerry Creative} -│   ├── LOOΠΔ 1_3 -│   │   └── LOOΠΔ 1_3 - 2017. Love & Evil [K-Pop] {BlockBerry Creative} -│   ├── LOOΠΔ ODD EYE CIRCLE -│   │   └── LOOΠΔ ODD EYE CIRCLE - 2017. Max & Match [K-Pop] {BlockBerry Creative} -│   └── YUZION -│   └── YUZION - 2019. Young Trapper [Hip Hop] {No Label} -├── Genres -│   ├── Hip-Hop -│   │   └── YUZION - 2019. Young Trapper [Hip Hop] {No Label} -│   └── K-Pop -│      ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment} -│      ├── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment} -│      ├── LOOΠΔ 1_3 - 2017. Love & Evil [K-Pop] {BlockBerry Creative} -│      ├── LOOΠΔ - 2019. [X X] [K-Pop] {BlockBerry Creative} -│      ├── LOOΠΔ - 2020. [#] [K-Pop] {BlockBerry Creative} -│      └── LOOΠΔ ODD EYE CIRCLE - 2017. Max & Match [K-Pop] {BlockBerry Creative} -└── Labels - ├── BlockBerry Creative - │   ├── LOOΠΔ 1_3 - 2017. Love & Evil [K-Pop] {BlockBerry Creative} - │   ├── LOOΠΔ - 2019. [X X] [K-Pop] {BlockBerry Creative} - │   ├── LOOΠΔ - 2021. [&] [K-Pop] {BlockBerry Creative} - │   └── LOOΠΔ ODD EYE CIRCLE - 2017. Max & Match [K-Pop] {BlockBerry Creative} - ├── No Label -    │   └── YUZION - 2019. Young Trapper [Hip Hop] {No Label} - └── YG Entertainment -       ├── BLACKPINK - 2016. SQUARE ONE - Single [K-Pop] {YG Entertainment} - └── BLACKPINK - 2016. SQUARE TWO - Single [K-Pop] {YG Entertainment} +```bash +$ nix profile install github:azuline/rose#rose ``` -## The Metadata Improvement Tooling - -Rosé constructs the virtual filesystem from the audio tags. However, audio tags -are frequently missing or incorrect. Thus, Rosé also provides a set of tools to -improve the audio tag metadata. - -Note that the metadata manager _modifies_ the source files. If you do not want -to modify the source files, you should `chmod 444` and not use the metadata -manager! +In the future, other packaging systems may be considered. However, I strongly +dislike Python's packaging story, hence: Nix. -I have yet to write this part of the tool. Please check back later! +## Quickstart +After installing Rosé, let's first confirm that we can invoke the tool. Rosé +provides the `rose` CLI tool, which should emit help text when ran. -# Usage +```bash +$ rose -``` -Usage: python -m rose [OPTIONS] COMMAND [ARGS]... +Usage: rose [OPTIONS] COMMAND [ARGS]... A virtual filesystem for music and metadata improvement tooling. @@ -163,7 +173,17 @@ Commands: playlists Manage playlists. ``` -## Supported Filetypes +Next... + +- Mount +- Play! +- Unmount + +## Features + +TODO + +## Requirements Rosé supports `.mp3`, `.m4a`, `.ogg` (vorbis), `.opus`, and `.flac` audio files. @@ -171,19 +191,19 @@ Rosé also supports JPEG and PNG cover art. The supported cover art file stems are `cover`, `folder`, and `art`. The supported cover art file extensions are `.jpg`, `.jpeg`, and `.png`. -## Virtual Filesystem +## License -The virtual filesystem is mounted and unmounted by `rose fs mount` and -`rose fs unmount` respectively. +Copyright 2023 blissful -TODO +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0. -- document supported operations +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. -## Metadata Management +## Contributions TODO - -## Systemd Unit Files - -TODO; example unit files to schedule Rosé with systemd. diff --git a/rose/virtualfs.py b/rose/virtualfs.py index 46479e7..b036ba9 100644 --- a/rose/virtualfs.py +++ b/rose/virtualfs.py @@ -359,7 +359,8 @@ def readdir(self, path: str, _: int) -> Iterator[str]: if p.view == "Collages" and p.collage: releases = list(list_collage_releases(self.config, p.collage)) - pad_size = max(len(str(r[0])) for r in releases) + # Two zeros because `max(single_arg)` assumes that the single_arg is enumerable. + pad_size = max(0, 0, *[len(str(r[0])) for r in releases]) for idx, virtual_dirname, source_dir in releases: v = f"{str(idx).zfill(pad_size)}. {virtual_dirname}" yield v @@ -378,7 +379,7 @@ def readdir(self, path: str, _: int) -> Iterator[str]: if pdata is None: raise fuse.FuseOSError(errno.ENOENT) playlist, tracks = pdata - pad_size = max([len(str(i + 1)) for i, _ in enumerate(tracks)]) + pad_size = max(0, 0, *[len(str(i + 1)) for i, _ in enumerate(tracks)]) for idx, track in enumerate(tracks): v = f"{str(idx+1).zfill(pad_size)}. {track.virtual_filename}" yield v @@ -517,7 +518,7 @@ def release(self, path: str, fh: int) -> None: def mkdir(self, path: str, mode: int) -> None: logger.debug(f"Received mkdir for {path=} {mode=}") - p = parse_virtual_path(path) + p = parse_virtual_path(path, parse_release_position=False) logger.debug(f"Parsed mkdir path as {p}") # Possible actions: @@ -703,7 +704,7 @@ class ParsedPath: ADDED_AT_REGEX = re.compile(r"^\[[\d-]{10}\] ") -def parse_virtual_path(path: str) -> ParsedPath: +def parse_virtual_path(path: str, *, parse_release_position: bool = True) -> ParsedPath: parts = path.split("/")[1:] # First part is always empty string. if len(parts) == 1 and parts[0] == "": @@ -811,15 +812,19 @@ def parse_virtual_path(path: str) -> ParsedPath: return ParsedPath( view="Collages", collage=parts[1], - release=POSITION_REGEX.sub("", parts[2]), - release_position=m[1] if (m := POSITION_REGEX.match(parts[2])) else None, + release=POSITION_REGEX.sub("", parts[2]) if parse_release_position else parts[2], + release_position=m[1] + if parse_release_position and (m := POSITION_REGEX.match(parts[2])) + else None, ) if len(parts) == 4: return ParsedPath( view="Collages", collage=parts[1], - release=POSITION_REGEX.sub("", parts[2]), - release_position=m[1] if (m := POSITION_REGEX.match(parts[2])) else None, + release=POSITION_REGEX.sub("", parts[2]) if parse_release_position else parts[2], + release_position=m[1] + if parse_release_position and (m := POSITION_REGEX.match(parts[2])) + else None, file=POSITION_REGEX.sub("", parts[3]), file_position=m[1] if (m := POSITION_REGEX.match(parts[3])) else None, )