Skip to content

Commit

Permalink
Merge pull request #4 from adamjakab/devel
Browse files Browse the repository at this point in the history
Looking ahead - changed targets key structure
  • Loading branch information
adamjakab committed Feb 23, 2020
2 parents d729fa4 + 2f0f7b5 commit 447d232
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .idea/BeetsPluginGoingRunning.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 32 additions & 18 deletions README.md
Expand Up @@ -11,7 +11,18 @@ songs based on their speed(bpm) and duration and attempts to create a list of so

## Introduction

I in advance apologize for the following guide. I promise I will explain things a bit better at some point. Until then if something is not clear please use the Issue tracker.
To use this plugin at its best and to benefit the most from your library, you will need to make sure that you have
bpm information on all of your songs. Since this plugin uses the bpm information to select songs, the songs with bpm=0 will be ignored (check with `beet ls bpm:0`). If you have many you should update them. There are two ways:

1) use the built-in [acousticbrainz plugin](https://beets.readthedocs.io/en/stable/plugins/acousticbrainz.html) to fetch
the bpm information for your songs. It does a lot for well know songs but my library was still 30% uncovered after a full scan

2) Use the [bpmanalyser plugin](https://github.com/adamjakab/BeetsPluginBpmAnalyser). This will scan your songs and calculate
the tempo (bpm) value for them. If you have a big collection it might take a while, but you can potentially end up with
100% coverage.

The following explains how to use the *beets-goingrunning* plugin. If something is not clear please use the Issue tracker. Also, if there is a feature not present, please check the [roadmap](./ROADMAP.md) document to check if it is planned. If not, create a feature request in the Issue tracker.


## Installation
The plugin can be installed via:
Expand All @@ -28,7 +39,7 @@ plugins:
# [...]
```

Check if plugin is loaded with `beet version`. It should list 'bpmanalyser' amongst the loaded plugins.
Check if plugin is loaded with `beet version`. It should list 'goingrunning' amongst the loaded plugins.


## Usage
Expand All @@ -37,11 +48,13 @@ Invoke the plugin as:

$ beet goingrunning training_name [-lcq] [QUERY...]

There are the following switches available:
The following switches are available:

- --list [-l]: List all the configured trainings with their attributes. With this switch you do not enter the name of the training, just `beet goingrunning --list`
- --count [-c]: Count the number of songs available for a specific training. With `beet goingrunning longrun --count` you can see how many of your songs there are in your library that fit your specs.
- --quiet [-q]: Do not display any output from the command.
**--list [-l]**: List all the configured trainings with their attributes. With this switch you do not enter the name of the training, just `beet goingrunning --list`

**--count [-c]**: Count the number of songs available for a specific training. With `beet goingrunning longrun --count` you can see how many of your songs there are in your library that fit your specs.

**--quiet [-q]**: Do not display any output from the command.


## Configuration
Expand All @@ -52,28 +65,27 @@ goingrunning:
song_bpm: [90, 150]
song_len: [90, 240]
duration: 60
targets: {}
targets: []
target: no
clean_target: no
```

There are two concepts you need to know to configure the plugin: targets and trainings:

- Targets are named destinations on your file system to which you will be copying your songs. The `targets` key allows you to define multiple targets whilst the `target` key allows you to specify the name of your default player to which the plugin will always copy your songs (if not otherwise specified).
- **Targets** are named destinations on your file system to which you will be copying your songs. The `targets` key allows you to define multiple targets whilst the `target` key allows you to specify the name of your default player to which the plugin will always copy your songs (if not otherwise specified in the configuration of a specific training).

```yaml
goingrunning:
# [...]
targets:
my_player_1: /mnt/player_1
my_other_player: /media/player_2
- { name: my_player_1, device_path: /mnt/player_1/ }
- { name: my_other_player, device_path: /media/player_2 }
target: my_player_1
# [...]
```

- Trainings are not much more than named queries (for now - but I have some really cool plans) into your library. They
have two attributes by which the plugin will decide which songs to chose (`song_bpm` and `song_len`) and a `duration`
element (expressed in minutes) for deciding how many songs to select. The `song_bpm` and `song_len` attributes have two numbers which indicate the lower and the higher limit of that attribute.
- **Trainings** are not much more than named queries into your library. They have two main attributes (`song_bpm` and `song_len`) by which the plugin will decide which songs to chose and a `duration`
element (expressed in minutes) used for limiting the number of songs selected. The `song_bpm` and `song_len` attributes have two numbers which indicate the lower and the higher limit of that attribute. A training can optionally declare the `target` and other attributes to override those present at root level (directly under the `goingrunning` key).

A common configuration section will look something like this:

Expand All @@ -82,8 +94,8 @@ goingrunning:
# [...]
clean_target: no
targets:
my_player_1: /mnt/player_1
my_other_player: /media/player_2
- { name: my_player_1, device_path: /mnt/player_1/ }
- { name: my_other_player, device_path: /media/player_2 }
target: my_player_1
trainings:
longrun:
Expand All @@ -99,11 +111,13 @@ goingrunning:
# [...]
```

Once you have created your trainings you will just attach your player to your pc and launch:
Once you have configured your targets and created your trainings, connect your device to your pc and launch:

$ beet goingrunning 10K
$ beet goingrunning longrun

and you will always have your music with you that matches your training.
and the songs matching that training will be copied to it.

For now, within a training the selection of the songs is completely random and no ordering is applied. One of the future plans is to allow you to be more in control of the song selection and song ordering. You can of course use the usual query syntax to fine tune your selection (see examples below) but the ordering will still be casual.

All the configuration options are looked up in the entire configuration tree. This means that whilst the songs for the the `10K` training will be copied to the `my_other_player` target, the `longrun` training (which does not declare this attribute), will use that on the root level: `my_player_1`. This holds for all attributes.

Expand Down
34 changes: 34 additions & 0 deletions ROADMAP.md
@@ -0,0 +1,34 @@
# TODOS (ROADMAP)
This is a list of things that will be implemented at some point.

## Short term implementations

- stats - show statistics about the library - such as number of songs without bpm information
- training info: show total listening time for a specific training
- targets - target definition should include some extra info - just some ideas:
```yaml
goingrunning:
# [...]
targets:
-
name: SONY-1
device_path: /media/player_2
subfolder: MUSIC/AUTO
create_training_folder: yes
clean_destination: yes
create_playlist: yes
delete_extra_files:
- STDBDATA.DAT
# [...]
```
- generate playlist


## Long term implementations
- maximize unheard song proposal(optional) by:
- incrementing listen count on export
- adding it to the query and proposing songs with lower counts
- enable song merging and exporting all songs merged into one single file (optional)
- possibility to handle sections inside a training (for interval trainings / strides at different speeds)
- sections can also be repeated
- enable audio TTS files to give instructions during training: "Run for 10K at 4:45. RUN!" exporting it as mp3 files and adding it into the song list.
72 changes: 52 additions & 20 deletions beetsplug/goingrunning/command.py
Expand Up @@ -111,9 +111,8 @@ def handle_training(self):
total_time = GRC.get_duration_of_items(rnd_items)
# @todo: check if total time is close to duration - (config might be too restrictive or too few songs)

# Verify target path
target_path = self._get_target_path(training)
if not target_path:
# Verify target device path path
if not self._get_device_path_for_training(training):
return

# Show some info
Expand All @@ -127,23 +126,25 @@ def handle_training(self):
self._say("Run!")

def _clean_target_path(self, training: Subview):
if GRC.get_config_value_bubble_up(training, "clean_target"):
if self._get_target_attribute_for_training(training, "clean_target"):

target_name = GRC.get_config_value_bubble_up(training, "target")
target_path = self._get_target_path(training)
self._say("Cleaning target[{0}]: {1}".format(target_name, target_path))
device_path = self._get_device_path_for_training(training)

self._say("Cleaning target[{0}]: {1}".format(target_name, device_path))
song_extensions = ["mp3", "mp4", "flac", "wav"]
target_file_list = []
for ext in song_extensions:
target_file_list += glob(os.path.join(target_path, "*.{}".format(ext)))
target_file_list += glob(os.path.join(device_path, "*.{}".format(ext)))

for f in target_file_list:
self.log.debug("Deleting: {}".format(f))
os.remove(f)

def _copy_items_to_target(self, training: Subview, rnd_items):
target_name = GRC.get_config_value_bubble_up(training, "target")
target_path = self._get_target_path(training)
self._say("Copying to target[{0}]: {1}".format(target_name, target_path))
device_path = self._get_device_path_for_training(training)
self._say("Copying to target[{0}]: {1}".format(target_name, device_path))

def random_string(length=6):
letters = string.ascii_letters + string.digits
Expand All @@ -154,28 +155,59 @@ def random_string(length=6):
src = os.path.realpath(item.get("path").decode("utf-8"))
fn, ext = os.path.splitext(src)
gen_filename = "{0}_{1}{2}".format(str(cnt).zfill(6), random_string(), ext)
dst = "{0}/{1}".format(target_path, gen_filename)
dst = "{0}/{1}".format(device_path, gen_filename)
self.log.debug("Copying[{1}]: {0}".format(src, gen_filename))
copyfile(src, dst)
cnt += 1

def _get_target_path(self, training: Subview):
def _get_target_for_training(self, training: Subview):
target_name = GRC.get_config_value_bubble_up(training, "target")
targets = self.config["targets"].get()
self.log.debug("Checking target name: {0}".format(target_name))
self.log.debug("Finding target: {0}".format(target_name))
target: Subview = self.config["targets"][target_name]

if target_name not in targets.keys():
if not target.exists():
self._say("The target name[{0}] is not defined!".format(target_name))
return

target_path = os.path.realpath(Path(targets.get(target_name)).expanduser())
self.log.debug("Found target path: {0}".format(target_path))
return target

if not os.path.isdir(target_path):
self._say("The target[{0}] path does not exist: {1}".format(target_name, target_path))
def _get_target_attribute_for_training(self, training: Subview, attrib: str = "name"):
target_name = GRC.get_config_value_bubble_up(training, "target")
self.log.debug("Getting attribute[{0}] for target: {1}".format(attrib, target_name))
target = self._get_target_for_training(training)
if not target:
return

return target_path
# @todo: Needs checking and NotFound catching
if attrib == "name":
# this should NOT propagate up
attrib_val = target_name
if attrib == "device_path":
# this should NOT propagate up
attrib_val = target["device_path"].get()
else:
attrib_val = GRC.get_config_value_bubble_up(target, attrib)

self.log.debug("Found target[{0}] attribute[{1}] path: {2}".format(target_name, attrib, attrib_val))

return attrib_val

def _get_device_path_for_training(self, training: Subview):
target_name = GRC.get_config_value_bubble_up(training, "target")
device_path = self._get_target_attribute_for_training(training, "device_path")
if not device_path:
return

device_path = os.path.realpath(Path(device_path).expanduser())

if not os.path.isdir(device_path):
self._say("The target[{0}] path does not exist: {1}".format(target_name, device_path))
return

self.log.debug("Found target[{0}] path: {0}".format(target_name, device_path))

return device_path


def _retrieve_library_items(self, training: Subview):
query = []
Expand Down Expand Up @@ -215,7 +247,7 @@ def list_trainings(self):
# @todo: order keys
:return: void
"""
if "trainings" not in self.config:
if not self.config["trainings"].exists() or len(self.config["trainings"].keys()) == 0:
self._say("You have not created any trainings yet.")
return

Expand Down
21 changes: 8 additions & 13 deletions beetsplug/goingrunning/common.py
Expand Up @@ -29,23 +29,18 @@ def get_human_readable_time(seconds):
return "%d:%02d:%02d" % (h, m, s)


def get_config_value_bubble_up(target: Subview, attrib: str):
def get_config_value_bubble_up(cfg_view: Subview, attrib: str):
"""
Method that will ''bubble up'' in the configuration hierarchy to find the value of the requested attribute
"""
value = None
done = False

while not done:
tree: OrderedDict = target.flatten()
if attrib in tree:
value = tree.get(attrib)
done = True
else:
if target.root() != target.parent:
target: Subview = target.parent
else:
done = True

if cfg_view[attrib].exists():
value = cfg_view[attrib].get()
else:
view_name = cfg_view.name
if view_name != "root":
value = get_config_value_bubble_up(cfg_view.parent, attrib)

return value

Expand Down
2 changes: 1 addition & 1 deletion beetsplug/goingrunning/config_default.yml
Expand Up @@ -2,6 +2,6 @@ song_bpm: [90, 150]
song_len: [90, 240]
duration: 60
targets: {}
trainings: {}
target: no
clean_target: no

9 changes: 6 additions & 3 deletions dev.yml
Expand Up @@ -24,10 +24,13 @@ goingrunning:
song_len: [120, 240]
duration: 120
target: test
clean_target: false
targets:
test: ~/Documents/Projects/Python/BeetsPluginGoingRunning/tmp/songs/
SONY: /Volumes/WALKMAN/MUSIC/AUTO/
clean_target: true
- name: test
device_path: ~/Music/tmp/
clean_target: yes
- name: SONY
device_path: /Volumes/WALKMAN/MUSIC/AUTO/
trainings:
song_bpm: [11, 22]
halfmarathon:
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Expand Up @@ -17,7 +17,7 @@
# Setup
setup(
name='beets-goingrunning',
version='1.0.2',
version='1.0.3',
description='A beets plugin for creating and exporting songs matching your running session.',
long_description=README,
long_description_content_type='text/markdown',
Expand All @@ -39,6 +39,7 @@
],

tests_require=[
'pytest', 'nose', 'coverage',
'mock', 'six'
],

Expand Down

0 comments on commit 447d232

Please sign in to comment.