plex_symlinker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /spec/tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,3 @@
1
+ ruby_version: 2.6.1
2
+ ignore:
3
+ - plex_symlinker.gemspec
@@ -0,0 +1,11 @@
1
+ ---
2
+ services:
3
+ - docker
4
+
5
+ install: bin/ci-build-dev
6
+ script: bin/ci-test
7
+ deploy:
8
+ provider: script
9
+ script: bin/ci-build-production
10
+ on:
11
+ branch: master
@@ -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/
@@ -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}
@@ -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
@@ -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
@@ -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.
@@ -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).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ docker build -t "$DOCKER_HUB_REPOSITORY"-dev -f Dockerfile.dev .
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
6
+ docker build -t "$DOCKER_HUB_REPOSITORY" .
7
+ docker push "$DOCKER_HUB_REPOSITORY":latest
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ docker run -t "$DOCKER_HUB_REPOSITORY"-dev bundle exec standardrb
6
+ docker run -t "$DOCKER_HUB_REPOSITORY"-dev bundle exec rake
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,13 @@
1
+ module PlexSymlinker
2
+ class Symlink
3
+ attr_reader :path
4
+
5
+ def initialize(path)
6
+ @path = path
7
+ end
8
+
9
+ def target
10
+ File.readlink(path)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module PlexSymlinker
2
+ VERSION = "0.1.0"
3
+ 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: []