Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ FROM docker.io/python:3.8-alpine as builder

# install dependencies
RUN \
apk --no-cache add fuse fuse-dev flac \
apk --no-cache add fuse fuse-dev flac mp3splt \
&& /usr/local/bin/python -m pip install --upgrade pip

# enable non-root users to make FUSE fs non-private
Expand Down
50 changes: 50 additions & 0 deletions Dockerfile.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# =================================

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you have to introduce a new dockerfile for local testing. Have you tried the Dockerfile.dev instead? Dockerfile.dev would allow you to do live editing while testing without the need to rebuild the docker image after every change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried it, but I have some issues with it, I wasn't finding reasons why, also I need to start it in "production", and your Dokerfile depends on Pypi, so I've wrote my own)

I can remove it from pull request, If you want)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add some comments on how to use Docker.dev. Then we can decide if there is still a need for another one.

# Dockerfile for run trackfs from source locally
#
# Copyright 2020 by Andreas Schmidt
# All rights reserved.
# This file is part of the trackfs project
# and licensed under the terms of the GNU Lesser General Public License v3.0.
# See https://github.com/andresch/trackfs for details.
#
# =================================

FROM docker.io/python:3.8-alpine as builder

# install dependencies
RUN \
apk --no-cache add fuse fuse-dev flac mp3splt \
&& /usr/local/bin/python -m pip install --upgrade pip

# enable non-root users to make FUSE fs non-private
RUN echo "user_allow_other" >> /etc/fuse.conf

# FUSE requires that the user that mounts the FUSE filesystem
# has an entry in /etc/passwd
# Since we want to allow (and encourage) the usage of docker's
# --user option to run the container as non-root user,
# and with that don't know the uid of the user at build time
# we can't create the entry for that user at build time
# and also can't use adduser command during runtime as this would
# require root privileges.
# Instead we open /etc/passwd for writing.
# As /ets/shadow is still protected this should not cause harm,
# even if some attacker finds a way to take over the container

RUN chmod 666 /etc/passwd

COPY ./ /usr/src/trackfs
RUN pip install /usr/src/trackfs

# source directory containing flac+cue files
VOLUME /src

# mount point where to generate the tracks from the flac+cue files
VOLUME /dst

COPY launcher.sh /usr/local/bin/
RUN chmod 555 /usr/local/bin/launcher.sh

ENTRYPOINT ["/usr/local/bin/launcher.sh", "/src", "/dst"]


5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* FLAC files with embedded cuesheets (FLAC+CUE)
* FLAC files with accompanying cuesheet
* WAVE files with accompanying cuesheet
* MP3 files with accompanying cuesheet (embedded cuesheet not tested)

Accompanying cuesheets must use the fileextension `.cue` and have the same basename as album file.

Expand Down Expand Up @@ -38,7 +39,7 @@ Replace `/path/to/yourmusiclibrary` with the root directory where `trackfs` scan

Once started you will find all directories and files from your music library also in the `trackfs`-filesystem. Only album files got replaced: Instead of a single album file you will find individual FLAC files for each track found in the cue sheet. The track-files will have the following names:

{album-file}.#-#.{tracknumber}.{track-title}.flac
{album-file}.#-#.{tracknumber}.{track-title}.{source_ext}

While the tracks can be used like regular files, they don't exist in the physical file system on your machine. Instead `trackfs` creates them on the fly whenever an application starts loading any of the track files. This usually takes (depending on your system) a few seconds.

Expand Down Expand Up @@ -82,7 +83,7 @@ Instead it is recommended to let `trackfs` run as a regular user. For that to wo

`trackfs` provides a few options that allow you to tweak its default behavior:

* `-e EXTENSION`, `--extension EXTENSION` (default: "(\\.flac|\\.wav)") :
* `-e EXTENSION`, `--extension EXTENSION` (default: "(\\.flac|\\.wav|\\.mp3)") :
A regular expression matching file extension of album files in the music library
* `-s SEPARATOR`, `--separator SEPARATOR` (default: ".#-#."):
The separator used inside the name of the track-files. Must never occur in regular filenames
Expand Down
5 changes: 3 additions & 2 deletions README.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,18 @@ preconditions / has the following software installed:
* **[python](https://www.python.org/)**: use recent a python version (>=3.8) (trackfs is developped and tested with 3.8), including pip
* **[fuse](https://github.com/libfuse/libfuse)**: make sure that you have FUSE support enabled in your kernal and the FUSE libraries installed
* **[flac](https://xiph.org/flac/)**: make sure you have official flac binaries (flac and metaflac) installed and on your path
* **[mp3splt](http://mp3splt.sourceforge.net/)**: make sure you have official mp3splt binaries installed and on your path

On most recent debian based system you should get all dependencies with

```
sudo apt-get install python3 python3-pip fuse libfuse-dev flac
sudo apt-get install python3 python3-pip fuse libfuse-dev flac mp3splt
```

On alpine linux (used for the dockerized version of `trackfs`) you would use

```
sudo apk add python3 py3-pip fuse fuse-dev flac
sudo apk add python3 py3-pip fuse fuse-dev flac mp3splt
```

#### Verify that you have the expected python version
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.6
0.3.0
7 changes: 5 additions & 2 deletions trackfs/albuminfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from functools import lru_cache, cached_property
from typing import List, Optional

from mutagen import File
from mutagen import File, MutagenError
import chardet

from . import cuesheet
Expand All @@ -35,7 +35,10 @@ def __init__(self, path):

@cached_property
def meta(self) -> File:
return File(self.path)
try:
return File(self.path)
except MutagenError:
return type("DummyType", (object,), {'tags': None})()

def format(self) -> str:
return type(self.meta).__name__.upper()
Expand Down
3 changes: 3 additions & 0 deletions trackfs/cuesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ def seconds(self):
def flac_time(self):
return f'{self.mm:02d}:{self.ss:02d}.{int(100.0 / 75.0 * self.ff):02d}'

def mp3split_time(self):
return f'{self.mm:02d}.{self.ss:02d}.{int(100.0 / 75.0 * self.ff):02d}'

def __repr__(self):
return '%02d%02d%02d' % (self.mm, self.ss, self.ff)

Expand Down
91 changes: 69 additions & 22 deletions trackfs/flactracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from dataclasses import dataclass
from math import trunc
from tempfile import mkstemp
from subprocess import DEVNULL, run
from subprocess import DEVNULL, PIPE, Popen, run
from concurrent.futures import ThreadPoolExecutor
from typing import Tuple, Dict, Optional
from threading import RLock, Thread
Expand Down Expand Up @@ -138,8 +138,8 @@ def track_tags_as_flac_args(album_info, num):
return ' '.join([TrackManager._tag_as_flac_arg(k, v) for k, vs in tags.items() for v in vs])

@staticmethod
def _new_temp_filename() -> os.PathLike:
(fh, temp_file) = mkstemp()
def _new_temp_filename(**kwargs) -> os.PathLike:
(fh, temp_file) = mkstemp(**kwargs)
# we don't want to process the file in python; just want a unique filename
# that we let flac write the track into
# => close right away
Expand All @@ -166,26 +166,33 @@ def _extract_flac_track(self, path: os.PathLike, fp: FusePath, album_info: album

# extract picture from flac if available
picture_file = self._new_temp_filename()
metaflac_cmd = f'metaflac --export-picture-to="{picture_file}" "{fp.source}"'
log.debug(f'extracting picture with command: "{metaflac_cmd}"')
rc = run(metaflac_cmd, shell=True, stdout=None, stderr=DEVNULL).returncode
metaflac_cmd = ['metaflac', '--export-picture-to', picture_file, fp.source]
log.debug(f'extracting picture with command: "{" ".join(metaflac_cmd)}"')
rc = run(metaflac_cmd, stdout=None, stderr=DEVNULL).returncode
picture_arg = ""
if rc == 0:
picture_arg = f' --picture="{picture_file}"'
picture_arg = picture_file
else:
local_album_art = self._find_albmum_art(fp)
if local_album_art:
picture_arg = f' --picture="{local_album_art}"'
picture_arg = local_album_art

track = album_info.track(fp.num)
flac_cmd = (
f'flac -d --silent --stdout --skip={track.start.flac_time()}'
f' --until={track.end.flac_time()} "{fp.source}" '
f'| flac --silent -f --fast'
f' {self.track_tags_as_flac_args(album_info,fp.num)}{picture_arg} -o {track_file} -'
)
log.debug(f'extracting track with command: "{flac_cmd}"')
rc = run(flac_cmd, shell=True, stdout=None, stderr=DEVNULL).returncode
flac_cmd_in = ['flac', '-d', '--silent', '--stdout',
'--skip', track.start.flac_time(), '--until', track.end.flac_time(), fp.source]
ps = Popen(flac_cmd_in, stdout=PIPE)

flac_cmd_out = ['flac', '--silent', '-f', '--fast']
for k, vs in album_info.track_tags(fp.num).items():
for v in vs:
flac_cmd_out.extend(['--tag', f'{str(k).replace("=", " ")}={str(v).replace("=", " ")}'])
if picture_arg:
flac_cmd_out.extend(['--picture', picture_arg])
flac_cmd_out.extend(['-o', track_file, '-'])

log.debug(f'extracting track with command: "{" ".join(flac_cmd_in)} | {" ".join(flac_cmd_out)}"')
rc = run(flac_cmd_out, stdin=ps.stdout, stdout=None, stderr=DEVNULL).returncode

os.remove(picture_file)
with self.rwlock:
if rc != 0:
Expand Down Expand Up @@ -249,6 +256,36 @@ def _extract_wave_track(self, path: os.PathLike, fp: FusePath, album_info: album

return track_file

def _extract_mp3_track(self, path: os.PathLike, fp: FusePath, album_info: albuminfo.AlbumInfo) -> os.PathLike:
"""creates a real file for a given virtual track file

extracts the track from the underlying MP3+CUE file into
a temporary file and then opens the temporary file"""
log.info(f'open track "{path}"')

track_file = self._new_temp_filename(suffix='.mp3') # mp3splt adds .mp3 ext

track = album_info.track(fp.num)
mp3_cmd = [
'mp3splt', '-Q', '-d', os.path.dirname(track_file),
'-o', os.path.splitext(os.path.basename(track_file))[0],
'-g', f'r%[@o,@n={track.num},@t={track.title},@a={" ".join(track.artists or [])}]',
track.start.mp3split_time(), track.end.mp3split_time(), fp.source,
]

log.debug(f'extracting track with command: "{" ".join(mp3_cmd)}"')
rc = run(mp3_cmd, stdout=None, stderr=DEVNULL).returncode
with self.rwlock:
if rc != 0:
err_msg = f'failed to extract track #{fp.num} from file "{fp.source}"'
log.error(err_msg)
os.remove(track_file)
del self[path]
raise FlacSplitException(err_msg)
else:
self._add(path, track_file)
return track_file

def prepare_track(self, path: os.PathLike, fp: FusePath) -> os.PathLike:
log.info(f'prepare track "{path}"')
assert fp.is_track
Expand All @@ -271,6 +308,8 @@ def prepare_track(self, path: os.PathLike, fp: FusePath) -> os.PathLike:
return self._extract_wave_track(path, fp, album_info)
elif audio_format == 'FLAC':
return self._extract_flac_track(path, fp, album_info)
elif audio_format == 'MP3':
return self._extract_mp3_track(path, fp, album_info)
else:
err_msg = f'unexpected audio format "{audio_format}"; can\'t proceed'
log.error(err_msg)
Expand Down Expand Up @@ -354,12 +393,20 @@ def estimate_track_file_size(self, path: os.PathLike, fp: FusePath) -> int:
album_info = albuminfo.get(fp.source)
meta = album_info.meta
track = album_info.track(fp.num)
return int(
(track.end - track.start).seconds()
* meta.info.channels
* (meta.info.bits_per_sample / 8)
* meta.info.sample_rate
)
if getattr(meta.info, 'bits_per_sample', None): # flac
return int(
(track.end - track.start).seconds()
* meta.info.channels
* (meta.info.bits_per_sample / 8)
* meta.info.sample_rate
)
elif getattr(meta.info, 'bitrate', None): # mp3
return int(
(track.end - track.start).seconds()
* meta.info.bitrate / 8
)
else:
return 0
else:
# use the actual size of the track-file
return os.stat(track_info.temp_file_path).st_size
19 changes: 9 additions & 10 deletions trackfs/fusepath.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@

DEFAULT_TRACK_SEPARATOR : str = '.#-#.'
DEFAULT_MAX_TITLE_LEN : int = 20
DEFAULT_ALBUM_EXTENSION : str = '(?i:\\.flac|\\.wav)'
DEFAULT_ALBUM_EXTENSION : str = '(?i:\\.flac|\\.wav|\\.mp3)'
DEFAULT_VALID_CHARS : str = "-_() " + string.ascii_letters + string.digits
DEFAULT_KEEP_ALBUM : bool = False
DEFAULT_TRACK_EXTENSION : str = '.flac'

@dataclass(frozen=True)
class Factory:
Expand All @@ -43,16 +42,14 @@ class Factory:
album_extension : str = DEFAULT_ALBUM_EXTENSION
valid_filename_chars : str = DEFAULT_VALID_CHARS
keep_album : bool = DEFAULT_KEEP_ALBUM
track_extension : bool = DEFAULT_TRACK_EXTENSION


@cached_property
def track_file_regex(self):
separator_rex = self.track_separator.replace('.','\\.')
track_exentension_rex = self.track_extension.replace('.','\\.')
flac_cue_rex = (
'^(?P<basename>.*)(?P<extension>'+self.album_extension+')'+separator_rex
+ '(?P<num>\\d+)(?P<title>(\\.[^\\.]{,'+str(self.max_title_len)
+ '}?)?)'+track_exentension_rex+'$'
+ '}?)?)\\.[^.]+$'
)
log.debug("Factory.track_file_regex: "+flac_cue_rex)
return re.compile(flac_cue_rex)
Expand Down Expand Up @@ -86,6 +83,10 @@ def from_track(self, source_root, extension, track):

@dataclass(frozen=True)
class FusePath:
VEXTENSION = {
# 'mpeg3': 'mp3', possible use
'wav': 'flac' # all wav files saves with flac extension to preserve metadata
}
''' represents an entry in the virtual trackfs filesystem'''
source_root : str
extension : str
Expand All @@ -108,8 +109,6 @@ def track_file_regex(self): return self._factory.track_file_regex
def album_ext_regex(self): return self._factory.album_ext_regex
@property
def keep_album(self): return self._factory.keep_album
@property
def track_extension(self): return self._factory.track_extension

@cached_property
def source(self):
Expand All @@ -129,9 +128,9 @@ def vpath(self):
if(self.is_track):
return (
f'{self.source_root}{self.extension}{self.track_separator}{self.num:03d}'
f'{self.title_fragment}{self.track_extension}'
f'{self.title_fragment}{self.VEXTENSION.get(self.extension, self.extension)}'
)
else:
else:
return self.source

def dirname(self):
Expand Down