plex_symlinker 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/.travis.yml +11 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +18 -0
- data/Dockerfile.dev +16 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +91 -0
- data/LICENSE.txt +21 -0
- data/README.md +85 -0
- data/Rakefile +6 -0
- data/bin/ci-build-dev +3 -0
- data/bin/ci-build-production +7 -0
- data/bin/ci-test +6 -0
- data/bin/console +11 -0
- data/bin/docker-plex_symlinker +7 -0
- data/bin/setup +8 -0
- data/exe/plex_symlinker +21 -0
- data/lib/plex_symlinker.rb +27 -0
- data/lib/plex_symlinker/file_types/audio_file.rb +76 -0
- data/lib/plex_symlinker/file_types/mp3.rb +37 -0
- data/lib/plex_symlinker/file_types/mp4.rb +45 -0
- data/lib/plex_symlinker/operation.rb +100 -0
- data/lib/plex_symlinker/symlink.rb +13 -0
- data/lib/plex_symlinker/version.rb +3 -0
- data/plex_symlinker.gemspec +32 -0
- metadata +144 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2451ea81acfe07abdde34f95d0d4f8170254d2caf711b7de8c6350ee42e331ea
|
|
4
|
+
data.tar.gz: a36385ba8fb204f8d1f1ee742318e9fa1b9eeb8485833b389c6adffe676a03d3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 314f13fbd52f6923991d91e5637484ab771a5ad9abc4a5bb2a5ceb802c8132ab76dd25414c5ea34927ac6d0ba718520077199032ad801d31e9fb0087411fee01
|
|
7
|
+
data.tar.gz: 49b3fb4632f59cbef70171e512f589312933bce9ab822b33d12a850ba44673274598addbd86786721633f3f97d57fadbfcf1fa6f78c50a07d0a03ee12a617087
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.standard.yml
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at stex@sterex.de. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at [https://contributor-covenant.org/version/1/4][version]
|
|
72
|
+
|
|
73
|
+
[homepage]: https://contributor-covenant.org
|
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/
|
data/Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
FROM ruby:slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && \
|
|
4
|
+
apt-get install -y libtag1-dev libtag-extras-dev g++ make && \
|
|
5
|
+
apt-get clean
|
|
6
|
+
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
COPY ./lib/plex_symlinker/version.rb ./lib/plex_symlinker/version.rb
|
|
10
|
+
COPY ./Gemfile* ./
|
|
11
|
+
COPY ./plex_symlinker.gemspec ./
|
|
12
|
+
|
|
13
|
+
RUN bundle config set deployment 'true'
|
|
14
|
+
RUN bundle install --without=development
|
|
15
|
+
|
|
16
|
+
COPY ./ ./
|
|
17
|
+
|
|
18
|
+
CMD bundle exec exe/plex_symlinker /app/source /app/target --symlink-target-dir=${SYMLINK_TARGET_DIR}
|
data/Dockerfile.dev
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
FROM ruby:slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && \
|
|
4
|
+
apt-get install -y libtag1-dev libtag-extras-dev g++ make
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
COPY ./lib/plex_symlinker/version.rb ./lib/plex_symlinker/version.rb
|
|
9
|
+
COPY ./Gemfile* ./
|
|
10
|
+
COPY ./plex_symlinker.gemspec ./
|
|
11
|
+
|
|
12
|
+
RUN bundle install
|
|
13
|
+
|
|
14
|
+
COPY ./ ./
|
|
15
|
+
|
|
16
|
+
CMD bundle exec rake
|
data/Gemfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
source "https://rubygems.org"
|
|
2
|
+
|
|
3
|
+
# Specify your gem's dependencies in plex_symlinker.gemspec
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
group :development do
|
|
7
|
+
gem "rake", "~> 12.0"
|
|
8
|
+
gem "rspec", "~> 3.0"
|
|
9
|
+
gem "rspec-its", "~> 1.3"
|
|
10
|
+
gem "pry", "~> 0.13.1"
|
|
11
|
+
gem "standard", "~> 0.4"
|
|
12
|
+
end
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
plex_symlinker (0.1.0)
|
|
5
|
+
activesupport (~> 6.0)
|
|
6
|
+
ruby-progressbar (~> 1.10)
|
|
7
|
+
slop (~> 4.8)
|
|
8
|
+
taglib-ruby (~> 1.0)
|
|
9
|
+
zeitwerk (~> 2.4)
|
|
10
|
+
|
|
11
|
+
GEM
|
|
12
|
+
remote: https://rubygems.org/
|
|
13
|
+
specs:
|
|
14
|
+
activesupport (6.0.3.2)
|
|
15
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
16
|
+
i18n (>= 0.7, < 2)
|
|
17
|
+
minitest (~> 5.1)
|
|
18
|
+
tzinfo (~> 1.1)
|
|
19
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
20
|
+
ast (2.4.1)
|
|
21
|
+
coderay (1.1.3)
|
|
22
|
+
concurrent-ruby (1.1.6)
|
|
23
|
+
diff-lcs (1.4.4)
|
|
24
|
+
i18n (1.8.4)
|
|
25
|
+
concurrent-ruby (~> 1.0)
|
|
26
|
+
method_source (1.0.0)
|
|
27
|
+
minitest (5.14.1)
|
|
28
|
+
parallel (1.19.2)
|
|
29
|
+
parser (2.7.1.4)
|
|
30
|
+
ast (~> 2.4.1)
|
|
31
|
+
pry (0.13.1)
|
|
32
|
+
coderay (~> 1.1)
|
|
33
|
+
method_source (~> 1.0)
|
|
34
|
+
rainbow (3.0.0)
|
|
35
|
+
rake (12.3.3)
|
|
36
|
+
regexp_parser (1.7.1)
|
|
37
|
+
rexml (3.2.4)
|
|
38
|
+
rspec (3.9.0)
|
|
39
|
+
rspec-core (~> 3.9.0)
|
|
40
|
+
rspec-expectations (~> 3.9.0)
|
|
41
|
+
rspec-mocks (~> 3.9.0)
|
|
42
|
+
rspec-core (3.9.2)
|
|
43
|
+
rspec-support (~> 3.9.3)
|
|
44
|
+
rspec-expectations (3.9.2)
|
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
46
|
+
rspec-support (~> 3.9.0)
|
|
47
|
+
rspec-its (1.3.0)
|
|
48
|
+
rspec-core (>= 3.0.0)
|
|
49
|
+
rspec-expectations (>= 3.0.0)
|
|
50
|
+
rspec-mocks (3.9.1)
|
|
51
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
52
|
+
rspec-support (~> 3.9.0)
|
|
53
|
+
rspec-support (3.9.3)
|
|
54
|
+
rubocop (0.85.1)
|
|
55
|
+
parallel (~> 1.10)
|
|
56
|
+
parser (>= 2.7.0.1)
|
|
57
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
58
|
+
regexp_parser (>= 1.7)
|
|
59
|
+
rexml
|
|
60
|
+
rubocop-ast (>= 0.0.3)
|
|
61
|
+
ruby-progressbar (~> 1.7)
|
|
62
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
|
63
|
+
rubocop-ast (0.1.0)
|
|
64
|
+
parser (>= 2.7.0.1)
|
|
65
|
+
rubocop-performance (1.6.1)
|
|
66
|
+
rubocop (>= 0.71.0)
|
|
67
|
+
ruby-progressbar (1.10.1)
|
|
68
|
+
slop (4.8.2)
|
|
69
|
+
standard (0.4.7)
|
|
70
|
+
rubocop (~> 0.85.0)
|
|
71
|
+
rubocop-performance (~> 1.6.0)
|
|
72
|
+
taglib-ruby (1.0.1)
|
|
73
|
+
thread_safe (0.3.6)
|
|
74
|
+
tzinfo (1.2.7)
|
|
75
|
+
thread_safe (~> 0.1)
|
|
76
|
+
unicode-display_width (1.7.0)
|
|
77
|
+
zeitwerk (2.4.0)
|
|
78
|
+
|
|
79
|
+
PLATFORMS
|
|
80
|
+
ruby
|
|
81
|
+
|
|
82
|
+
DEPENDENCIES
|
|
83
|
+
plex_symlinker!
|
|
84
|
+
pry (~> 0.13.1)
|
|
85
|
+
rake (~> 12.0)
|
|
86
|
+
rspec (~> 3.0)
|
|
87
|
+
rspec-its (~> 1.3)
|
|
88
|
+
standard (~> 0.4)
|
|
89
|
+
|
|
90
|
+
BUNDLED WITH
|
|
91
|
+
2.1.4
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Stefan Exner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# PlexSymlinker
|
|
2
|
+
|
|
3
|
+
This gem allows creating a Plex-friendly folder structure with symlinks for your audio books.
|
|
4
|
+
All you need are audio files with correct tagging, the gem takes care of making Plex understand them without you having to change the way you're organising your files.
|
|
5
|
+
|
|
6
|
+
**The Problem**
|
|
7
|
+
|
|
8
|
+
Most of my audio files are in apple's `m4b` format, leaving me with one file per book most of the times:
|
|
9
|
+
|
|
10
|
+
📁 audiobooks
|
|
11
|
+
∟ 📁 author name
|
|
12
|
+
∟ book1.m4b
|
|
13
|
+
∟ book2.m4b
|
|
14
|
+
|
|
15
|
+
The problem with this structure is that Plex' music agent doesn't quite understand it.
|
|
16
|
+
Even though the files are properly tagged with author, album, etc., Plex tends to create a giant
|
|
17
|
+
album out of all the files inside - often with the first audio book as album name.
|
|
18
|
+
|
|
19
|
+
What the Plex music agent expects is a structure like this:
|
|
20
|
+
|
|
21
|
+
📁 audiobooks
|
|
22
|
+
∟ 📁 author name
|
|
23
|
+
∟ 📁 Book 1
|
|
24
|
+
∟ book1.m4b
|
|
25
|
+
∟ 📁 Book 2
|
|
26
|
+
∟ book2.m4b
|
|
27
|
+
|
|
28
|
+
This would mean that I'd have to introduce single-file-directories into my structure which didn't really make
|
|
29
|
+
sense for me - especially as I have a lot of "Hörspiele" (mostly german format of short audio books with multiple
|
|
30
|
+
actors + music).
|
|
31
|
+
|
|
32
|
+
**This gem's solution**
|
|
33
|
+
|
|
34
|
+
`plex_symlinker` creates symlinks pointing to your actual audio files in exactly the structure Plex' music agent
|
|
35
|
+
expects to find. It uses the embedded tags in your files to build it, so even one big directory with
|
|
36
|
+
all your audio files in it would work as expected.
|
|
37
|
+
|
|
38
|
+
Just point plex to the symlink directory instead of your actual files and you're good to go.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
There are 2 ways to install/use this gem:
|
|
43
|
+
|
|
44
|
+
**Option 1: Install the gem on your local machine and run it directly**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
gem install plex_symlinker
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
As this gem makes use of `taglib-ruby`, you have to make sure that the necessary packages are installed on your machine.
|
|
51
|
+
Please refer to [robinst/taglib-ruby](https://github.com/robinst/taglib-ruby#installation) for more information.
|
|
52
|
+
|
|
53
|
+
**Option 2: Use the docker image**
|
|
54
|
+
|
|
55
|
+
A [docker image](https://hub.docker.com/r/sterexx/plex_symlinker) is available which includes
|
|
56
|
+
all necessary dependencies and can be used out-of-the-box by mounting the necessary directories.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Using the gem executable
|
|
61
|
+
|
|
62
|
+
### Using the Docker image
|
|
63
|
+
|
|
64
|
+
Please refer to [Docker Hub](https://hub.docker.com/r/sterexx/plex_symlinker) for instructions
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
69
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
70
|
+
|
|
71
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
72
|
+
|
|
73
|
+
## Contributing
|
|
74
|
+
|
|
75
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/stex/plex_symlinker.
|
|
76
|
+
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/stex/plex_symlinker/blob/master/CODE_OF_CONDUCT.md).
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
81
|
+
|
|
82
|
+
## Code of Conduct
|
|
83
|
+
|
|
84
|
+
Everyone interacting in the PlexSymlinker project's codebases, issue trackers,
|
|
85
|
+
chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/stex/plex_symlinker/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/ci-build-dev
ADDED
data/bin/ci-test
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "plex_symlinker"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
require "pry"
|
|
11
|
+
Pry.start
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
echo "Creating target directory with current user permissions..."
|
|
4
|
+
mkdir -p $2
|
|
5
|
+
|
|
6
|
+
echo "Running plex_symlinker in Docker..."
|
|
7
|
+
docker run -it --user "$(id -u):$(id -g)" --rm -v "$1":/app/source:ro -v "$2":/app/target --env SYMLINK_TARGET_DIR="$1" sterexx/plex_symlinker
|
data/bin/setup
ADDED
data/exe/plex_symlinker
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "plex_symlinker"
|
|
5
|
+
require "slop"
|
|
6
|
+
|
|
7
|
+
opts = Slop.parse { |o|
|
|
8
|
+
o.string "--symlink-target-dir", "Overrides the base dir in symlink targets"
|
|
9
|
+
o.string "--log", "Log output path"
|
|
10
|
+
o.on "--help" do
|
|
11
|
+
puts o
|
|
12
|
+
exit
|
|
13
|
+
end
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fail ArgumentError, "Please provide at least source and target directory." if opts.arguments.size != 2
|
|
17
|
+
if opts[:log]
|
|
18
|
+
PlexSymlinker.output = File.new(opts[:log], "w")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
PlexSymlinker::Operation.new(*opts.arguments, symlink_target_dir: opts[:symlink_target_dir]).perform
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require "rubygems"
|
|
2
|
+
require "bundler/setup"
|
|
3
|
+
require "active_support/all"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "ruby-progressbar"
|
|
6
|
+
require "taglib"
|
|
7
|
+
require "zeitwerk"
|
|
8
|
+
|
|
9
|
+
loader = Zeitwerk::Loader.for_gem
|
|
10
|
+
loader.setup
|
|
11
|
+
loader.eager_load
|
|
12
|
+
|
|
13
|
+
module PlexSymlinker
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
def self.output
|
|
17
|
+
@output ||= STDOUT
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.output=(val)
|
|
21
|
+
@output = val
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.logger
|
|
25
|
+
@logger ||= Logger.new(output)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module PlexSymlinker
|
|
2
|
+
module FileTypes
|
|
3
|
+
class AudioFile
|
|
4
|
+
class << self
|
|
5
|
+
def registered_types
|
|
6
|
+
@registered_types ||= {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def register_type(extension, klass)
|
|
10
|
+
registered_types[extension.to_s] = klass
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tag_reader(method_name, tag)
|
|
14
|
+
define_method(method_name) do
|
|
15
|
+
tags[tag.to_s]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def from_path(path)
|
|
20
|
+
extension = File.extname(path)[1..-1]
|
|
21
|
+
registered_types[extension.to_s]&.new(path) || fail(ArgumentError, "No type handler registered for extension '#{extension}'")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :path
|
|
26
|
+
|
|
27
|
+
def initialize(path)
|
|
28
|
+
@path = path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def tags
|
|
32
|
+
fail NotImplementedError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def album
|
|
36
|
+
fail NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def genre
|
|
40
|
+
fail NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def title
|
|
44
|
+
fail NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def year
|
|
48
|
+
fail NotImplementedError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def artist
|
|
52
|
+
fail NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def album_artist
|
|
56
|
+
fail NotImplementedError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def track_number
|
|
60
|
+
fail NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def symlink_file_name
|
|
64
|
+
File.basename(path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def relative_symlink_dir
|
|
68
|
+
"#{album_artist}/#{album}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def relative_symlink_path
|
|
72
|
+
"#{relative_symlink_dir}/#{symlink_file_name}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module PlexSymlinker
|
|
2
|
+
module FileTypes
|
|
3
|
+
# {
|
|
4
|
+
# "TDRC": "1968",
|
|
5
|
+
# "TIT2": "Seite B",
|
|
6
|
+
# "TKEY": "C",
|
|
7
|
+
# "TBPM": "120",
|
|
8
|
+
# "TPE1": "Winnetou",
|
|
9
|
+
# "TALB": "Winnetou 3 - 3. Folge",
|
|
10
|
+
# "TPE2": "alb-artist: Winnetou",
|
|
11
|
+
# "TCON": "Hörspiel",
|
|
12
|
+
# "APIC": "[image/jpeg]",
|
|
13
|
+
# "TRCK": "2/2",
|
|
14
|
+
# "TXXX": "[PERFORMER_NAME] PERFORMER_NAME a performer"
|
|
15
|
+
# }
|
|
16
|
+
class Mp3 < AudioFile
|
|
17
|
+
tag_reader :album, "TALB"
|
|
18
|
+
tag_reader :genre, "TCON"
|
|
19
|
+
tag_reader :title, "TIT2"
|
|
20
|
+
tag_reader :year, "TDRC"
|
|
21
|
+
tag_reader :artist, "TPE1"
|
|
22
|
+
tag_reader :track_number, "TRCK"
|
|
23
|
+
|
|
24
|
+
def tags
|
|
25
|
+
@tags ||= TagLib::MPEG::File.open(path) { |file|
|
|
26
|
+
Hash[file.id3v2_tag.frame_list.map { |f| [f.frame_id, f.to_s] }]
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def album_artist
|
|
31
|
+
tags["TPE2"] || artist
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
PlexSymlinker::FileTypes::AudioFile.register_type :mp3, PlexSymlinker::FileTypes::Mp3
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module PlexSymlinker
|
|
2
|
+
module FileTypes
|
|
3
|
+
# => {"----:com.apple.iTunes:ALBUM ARTIST"=>"Walter Moers",
|
|
4
|
+
# "----:com.apple.iTunes:Encoding Params"=>"Nero AAC codec / Aug 6 2007",
|
|
5
|
+
# "----:com.apple.iTunes:PERFORMER"=>"Andreas Fröhlich",
|
|
6
|
+
# "----:com.apple.iTunes:cdec"=>"Nero AAC codec / Aug 6 2007",
|
|
7
|
+
# "----:com.apple.iTunes:tool"=>"Nero AAC codec / Aug 6 2007",
|
|
8
|
+
# "aART"=>"Walter Moers",
|
|
9
|
+
# "covr"=>nil,
|
|
10
|
+
# "soaa"=>"Moers, Walter",
|
|
11
|
+
# "soal"=>"05 - Der Schrecksenmeister",
|
|
12
|
+
# "soar"=>"Moers, Walter",
|
|
13
|
+
# "trkn"=>nil,
|
|
14
|
+
# "©ART"=>"Walter Moers",
|
|
15
|
+
# "©alb"=>"Der Schreckensmeister",
|
|
16
|
+
# "©day"=>"2008",
|
|
17
|
+
# "©gen"=>"Hörbuch",
|
|
18
|
+
# "©nam"=>"Der Schreckensmeister - Teil 1",
|
|
19
|
+
# "©too"=>"Nero AAC codec / Aug 6 2007"}
|
|
20
|
+
|
|
21
|
+
class Mp4 < AudioFile
|
|
22
|
+
tag_reader :sort_album_artist, "soaa"
|
|
23
|
+
tag_reader :sort_artist, "soar"
|
|
24
|
+
tag_reader :sort_album, "soal"
|
|
25
|
+
tag_reader :album, "©alb"
|
|
26
|
+
tag_reader :genre, "©gen"
|
|
27
|
+
tag_reader :title, "©nam"
|
|
28
|
+
tag_reader :year, "©day"
|
|
29
|
+
tag_reader :artist, "©ART"
|
|
30
|
+
tag_reader :performer, "----:com.apple.iTunes:PERFORMER_NAME"
|
|
31
|
+
|
|
32
|
+
def tags
|
|
33
|
+
@tags ||= TagLib::MP4::File.open(path) { |file|
|
|
34
|
+
Hash[file.tag.item_map.to_a.map { |k, i| [k, i.to_string_list.first] }]
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def album_artist
|
|
39
|
+
tags["aART"] || artist
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
PlexSymlinker::FileTypes::AudioFile.register_type :m4b, PlexSymlinker::FileTypes::Mp4
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module PlexSymlinker
|
|
2
|
+
class Operation
|
|
3
|
+
attr_reader :files_base_dir, :symlinks_base_dir, :symlink_target_dir
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# @param [String] files_base_dir
|
|
7
|
+
# The directory the audio files are in. Sub-Directories are scanned accordingly.
|
|
8
|
+
# @param [String] symlinks_base_dir
|
|
9
|
+
# The directory the symlinks and folder structure should be placed in.
|
|
10
|
+
# @param [String] symlink_target_dir
|
|
11
|
+
# In some cases, the created symlinks should not target the actual files directory,
|
|
12
|
+
# but a different one instead. An example would be the usage of this Gem through a Dockerfile
|
|
13
|
+
# with mounted volumes.
|
|
14
|
+
#
|
|
15
|
+
# @example Usage with a custom symlink target dir
|
|
16
|
+
# # When using a docker container to run this gem, we have to make sure it has access to
|
|
17
|
+
# # both the files and the directory to place the symlinks in. The easiest way to do so
|
|
18
|
+
# # is by mounting both directories as volumes.
|
|
19
|
+
# # The docker image expects the files and symlinks directories to be mounted as /app/source and /app/target
|
|
20
|
+
# # respectively:
|
|
21
|
+
#
|
|
22
|
+
# docker run plex-symlinker -v /path/to/audio/files:/app/source -v /path/to/symlinks:/app/target
|
|
23
|
+
#
|
|
24
|
+
# # The problem here would be that the generated symlinks would all point to files inside /app/source
|
|
25
|
+
# # instead of the actual audio file directory as this is the only directly that's known inside the
|
|
26
|
+
# # docker container.
|
|
27
|
+
# # To fix this, specifying the actual files directory outside the container as +symlink_target_dir+
|
|
28
|
+
# # takes precedence when creating the symlinks. This is already done in /bin/plex-symlinker-docker.
|
|
29
|
+
# # An example initialization for the above paths would therefore be:
|
|
30
|
+
#
|
|
31
|
+
# Operation.new("/app/source", "/app/target", symlink_target_dir: "/path/to/audio/files")
|
|
32
|
+
#
|
|
33
|
+
def initialize(files_base_dir, symlinks_base_dir, symlink_target_dir: nil)
|
|
34
|
+
@files_base_dir = files_base_dir
|
|
35
|
+
@symlinks_base_dir = symlinks_base_dir
|
|
36
|
+
@symlink_target_dir = symlink_target_dir.presence || @files_base_dir
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# Searches for files within the given directory and its subdirectories.
|
|
41
|
+
#
|
|
42
|
+
# @param [String] dir
|
|
43
|
+
# The directory to search in
|
|
44
|
+
# @param [Array<#to_s>] extensions
|
|
45
|
+
# File extensions to be taken into account. Only files with a matching extension
|
|
46
|
+
# will be returned.
|
|
47
|
+
# @return [Array<String>] The paths to all files with matching extensions within the given directory
|
|
48
|
+
#
|
|
49
|
+
def files(dir, extensions = FileTypes::AudioFile.registered_types.keys)
|
|
50
|
+
Dir[File.join(dir, "**/*.{#{extensions.join(",")}}")]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def audio_files
|
|
54
|
+
files(files_base_dir).map(&FileTypes::AudioFile.method(:from_path))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def symlinks
|
|
58
|
+
files(symlinks_base_dir).map(&Symlink.method(:new))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def perform
|
|
62
|
+
cleanup
|
|
63
|
+
create_symlinks
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create_symlinks
|
|
67
|
+
PlexSymlinker.logger.info "Creating new symlinks..."
|
|
68
|
+
|
|
69
|
+
progress = ProgressBar.create(total: audio_files.size, output: PlexSymlinker.output)
|
|
70
|
+
|
|
71
|
+
audio_files.each do |file|
|
|
72
|
+
progress.log " --* #{file.album_artist}/#{file.album}..."
|
|
73
|
+
|
|
74
|
+
FileUtils.mkdir_p(File.join(symlinks_base_dir, file.relative_symlink_dir))
|
|
75
|
+
|
|
76
|
+
path = file.path.gsub(files_base_dir, symlink_target_dir)
|
|
77
|
+
symlink_path = File.join(symlinks_base_dir, file.relative_symlink_path)
|
|
78
|
+
|
|
79
|
+
# If we already have a symlink, don't try to create it again.
|
|
80
|
+
next if File.symlink?(symlink_path)
|
|
81
|
+
|
|
82
|
+
File.symlink(path, symlink_path)
|
|
83
|
+
progress.increment
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#
|
|
88
|
+
# Removes all symlinks from the target folder that don't have an existing target any more
|
|
89
|
+
#
|
|
90
|
+
# TODO: Remove empty directories as well
|
|
91
|
+
#
|
|
92
|
+
def cleanup
|
|
93
|
+
PlexSymlinker.logger.info "Removing invalid existing symlinks..."
|
|
94
|
+
symlinks.each do |link|
|
|
95
|
+
current_target = link.target.gsub(symlink_target_dir, files_base_dir)
|
|
96
|
+
File.delete(link.path) unless File.exist?(current_target)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative "lib/plex_symlinker/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "plex_symlinker"
|
|
5
|
+
spec.version = PlexSymlinker::VERSION
|
|
6
|
+
spec.authors = ["Stefan Exner"]
|
|
7
|
+
spec.email = ["stex@sterex.de"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "Create a plex music agent friendly symlink structure for your audiobook files. "
|
|
10
|
+
spec.description = "Create a plex music agent friendly symlink structure for your audiobook files. "
|
|
11
|
+
spec.homepage = "https://github.com/stex/plex_symlinker"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/stex/plex_symlinker"
|
|
17
|
+
|
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } rescue []
|
|
22
|
+
end
|
|
23
|
+
spec.bindir = "exe"
|
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
|
|
27
|
+
spec.add_runtime_dependency "activesupport", "~> 6.0"
|
|
28
|
+
spec.add_runtime_dependency "ruby-progressbar", "~> 1.10"
|
|
29
|
+
spec.add_runtime_dependency "slop", "~> 4.8"
|
|
30
|
+
spec.add_runtime_dependency "taglib-ruby", "~> 1.0"
|
|
31
|
+
spec.add_runtime_dependency "zeitwerk", "~> 2.4"
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: plex_symlinker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stefan Exner
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2020-07-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: ruby-progressbar
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.10'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.10'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: slop
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '4.8'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '4.8'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: taglib-ruby
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: zeitwerk
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '2.4'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '2.4'
|
|
83
|
+
description: 'Create a plex music agent friendly symlink structure for your audiobook
|
|
84
|
+
files. '
|
|
85
|
+
email:
|
|
86
|
+
- stex@sterex.de
|
|
87
|
+
executables:
|
|
88
|
+
- plex_symlinker
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- ".gitignore"
|
|
93
|
+
- ".rspec"
|
|
94
|
+
- ".standard.yml"
|
|
95
|
+
- ".travis.yml"
|
|
96
|
+
- CODE_OF_CONDUCT.md
|
|
97
|
+
- Dockerfile
|
|
98
|
+
- Dockerfile.dev
|
|
99
|
+
- Gemfile
|
|
100
|
+
- Gemfile.lock
|
|
101
|
+
- LICENSE.txt
|
|
102
|
+
- README.md
|
|
103
|
+
- Rakefile
|
|
104
|
+
- bin/ci-build-dev
|
|
105
|
+
- bin/ci-build-production
|
|
106
|
+
- bin/ci-test
|
|
107
|
+
- bin/console
|
|
108
|
+
- bin/docker-plex_symlinker
|
|
109
|
+
- bin/setup
|
|
110
|
+
- exe/plex_symlinker
|
|
111
|
+
- lib/plex_symlinker.rb
|
|
112
|
+
- lib/plex_symlinker/file_types/audio_file.rb
|
|
113
|
+
- lib/plex_symlinker/file_types/mp3.rb
|
|
114
|
+
- lib/plex_symlinker/file_types/mp4.rb
|
|
115
|
+
- lib/plex_symlinker/operation.rb
|
|
116
|
+
- lib/plex_symlinker/symlink.rb
|
|
117
|
+
- lib/plex_symlinker/version.rb
|
|
118
|
+
- plex_symlinker.gemspec
|
|
119
|
+
homepage: https://github.com/stex/plex_symlinker
|
|
120
|
+
licenses:
|
|
121
|
+
- MIT
|
|
122
|
+
metadata:
|
|
123
|
+
homepage_uri: https://github.com/stex/plex_symlinker
|
|
124
|
+
source_code_uri: https://github.com/stex/plex_symlinker
|
|
125
|
+
post_install_message:
|
|
126
|
+
rdoc_options: []
|
|
127
|
+
require_paths:
|
|
128
|
+
- lib
|
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: 2.3.0
|
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
requirements: []
|
|
140
|
+
rubygems_version: 3.1.4
|
|
141
|
+
signing_key:
|
|
142
|
+
specification_version: 4
|
|
143
|
+
summary: Create a plex music agent friendly symlink structure for your audiobook files.
|
|
144
|
+
test_files: []
|