iml 0.1.6 → 0.2.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 +5 -5
- data/.claude/settings.local.json +7 -0
- data/.github/workflows/ci.yml +30 -0
- data/.github/workflows/publish.yml +32 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +108 -50
- data/PLAN.md +350 -0
- data/README.md +79 -23
- data/Rakefile +9 -8
- data/TODO.md +43 -0
- data/bin/iml +80 -70
- data/gemspec.yml +7 -10
- data/iml.gemspec +22 -31
- data/lib/iml/configuration.rb +49 -0
- data/lib/iml/file_mover.rb +20 -0
- data/lib/iml/formatter.rb +16 -0
- data/lib/iml/media/movie.rb +30 -0
- data/lib/iml/media/tv_series.rb +45 -0
- data/lib/iml/normalizer.rb +79 -0
- data/lib/iml/parser.rb +39 -0
- data/lib/iml/pattern_builder.rb +42 -0
- data/lib/iml/version.rb +1 -1
- data/lib/iml.rb +34 -25
- data/patterns.yml +36 -1
- metadata +34 -72
- data/.rubocop.yml +0 -15
- data/.travis.yml +0 -7
- data/lib/iml/base.rb +0 -94
- data/lib/iml/hash.rb +0 -14
- data/lib/iml/movie.rb +0 -24
- data/lib/iml/patterns.rb +0 -64
- data/lib/iml/text.rb +0 -51
- data/lib/iml/tvseries.rb +0 -38
data/README.md
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
[](https://rubygems.org/gems/iml)
|
|
2
|
-
[](https://travis-ci.org/aladac/iml)
|
|
3
|
-
[](https://codeclimate.com/github/aladac/iml/maintainability)
|
|
4
|
-
[](https://codeclimate.com/github/aladac/iml/test_coverage)
|
|
5
|
-
[](https://www.rubydoc.info/github/aladac/iml/master)
|
|
6
2
|
|
|
7
3
|
[](https://rubygems.org/gems/iml)
|
|
8
4
|
|
|
9
5
|
*Intricate (Media) Matching Logic*
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
A media file handling library that detects the type of media file based on naming patterns.
|
|
8
|
+
It parses filenames into structured objects and renames files according to configurable format strings.
|
|
9
|
+
Includes an `iml` CLI for batch rename operations.
|
|
10
|
+
|
|
11
|
+
**Zero runtime dependencies.**
|
|
14
12
|
|
|
15
13
|
## Installation
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
Requires Ruby >= 3.1
|
|
18
16
|
|
|
19
17
|
Add this line to your application's Gemfile:
|
|
20
18
|
|
|
21
19
|
```ruby
|
|
22
|
-
gem
|
|
20
|
+
gem "iml"
|
|
23
21
|
```
|
|
24
22
|
|
|
25
23
|
And then execute:
|
|
@@ -38,24 +36,82 @@ Or install it yourself as:
|
|
|
38
36
|
Usage: iml [options] MEDIA_FILE [MEDIA_FILE] ...
|
|
39
37
|
-v, --[no-]verbose Run verbosely
|
|
40
38
|
-p, --[no-]pretend Dry run, do not move any files
|
|
41
|
-
-t, --target PATH
|
|
42
|
-
-o, --movie-format FORMAT
|
|
43
|
-
-O, --tv-format FORMAT
|
|
44
|
-
-l, --list-formats
|
|
45
|
-
-f, --force
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
-t, --target PATH Output directory (default: current directory)
|
|
40
|
+
-o, --movie-format FORMAT Movie output format (default: '%T (%Y).%f')
|
|
41
|
+
-O, --tv-format FORMAT TV series output format (default: '%T/Season %s/%T - S%SE%E.%f')
|
|
42
|
+
-l, --list-formats Show format placeholders
|
|
43
|
+
-f, --force Overwrite existing output files
|
|
44
|
+
-V, --version Show version
|
|
45
|
+
|
|
46
|
+
$ iml --pretend --verbose Some.Cool.Movie.2018.1080p.BRRip.x264.aac-GROUP.mp4
|
|
47
|
+
movie: Some.Cool.Movie.2018.1080p.BRRip.x264.aac-GROUP.mp4 -> Some Cool Movie (2018).mp4
|
|
48
|
+
done: 1 processed, 0 skipped
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
###
|
|
51
|
+
### Library
|
|
53
52
|
|
|
54
53
|
```ruby
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
require "iml"
|
|
55
|
+
|
|
56
|
+
# Parse a movie filename
|
|
57
|
+
movie = IML.parse("Cool.Movie.2018.720p.BluRay.H264.AAC2.0-GROUP.mp4")
|
|
58
|
+
movie.class #=> IML::Media::Movie
|
|
59
|
+
movie.title #=> "Cool Movie"
|
|
60
|
+
movie.year #=> "2018"
|
|
61
|
+
movie.codec #=> "h.264"
|
|
62
|
+
movie.source #=> "BD"
|
|
63
|
+
movie.movie? #=> true
|
|
64
|
+
|
|
65
|
+
# Parse a TV series filename
|
|
66
|
+
tv = IML.parse("Show.Name.S03E09.WEBRip.x264-GROUP.mkv")
|
|
67
|
+
tv.class #=> IML::Media::TvSeries
|
|
68
|
+
tv.title #=> "Show Name"
|
|
69
|
+
tv.season #=> "03"
|
|
70
|
+
tv.season_i #=> 3
|
|
71
|
+
tv.episode_i #=> 9
|
|
72
|
+
tv.tv? #=> true
|
|
73
|
+
|
|
74
|
+
# Format output paths
|
|
75
|
+
formatter = IML::Formatter.new
|
|
76
|
+
formatter.call(movie) #=> "Cool Movie (2018).mp4"
|
|
77
|
+
formatter.call(movie, format: "%T/%T.%Y.%f") #=> "Cool Movie/Cool Movie.2018.mp4"
|
|
78
|
+
formatter.pathname(movie, target: "/media") #=> #<Pathname:/media/Cool Movie (2018).mp4>
|
|
79
|
+
|
|
80
|
+
# Move files
|
|
81
|
+
mover = IML::FileMover.new
|
|
82
|
+
mover.call("source.mp4", movie, target: "/media", pretend: true)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Format Placeholders
|
|
86
|
+
|
|
87
|
+
| Placeholder | Description |
|
|
88
|
+
|-------------|-------------|
|
|
89
|
+
| `%T` | Title |
|
|
90
|
+
| `%t` | Episode title |
|
|
91
|
+
| `%Y` | Year |
|
|
92
|
+
| `%S` | Season (string, e.g. "03") |
|
|
93
|
+
| `%s` | Season (integer, e.g. "3") |
|
|
94
|
+
| `%E` | Episode (string, e.g. "09") |
|
|
95
|
+
| `%e` | Episode (integer, e.g. "9") |
|
|
96
|
+
| `%v` | Video codec |
|
|
97
|
+
| `%a` | Audio codec |
|
|
98
|
+
| `%q` | Quality |
|
|
99
|
+
| `%z` | Source |
|
|
100
|
+
| `%g` | Group |
|
|
101
|
+
| `%f` | File extension |
|
|
102
|
+
|
|
103
|
+
## Architecture
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
IML.parse(filename)
|
|
107
|
+
-> IML::Parser
|
|
108
|
+
-> IML::PatternBuilder (builds regex from patterns.yml)
|
|
109
|
+
-> IML::Normalizer (normalizes codecs, sources, titles)
|
|
110
|
+
-> IML::Media::Movie or IML::Media::TvSeries
|
|
111
|
+
|
|
112
|
+
IML::Formatter (formats output paths from media objects)
|
|
113
|
+
IML::FileMover (moves files using Formatter)
|
|
114
|
+
IML::Configuration (loads patterns.yml)
|
|
59
115
|
```
|
|
60
116
|
|
|
61
117
|
## License
|
data/Rakefile
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "standard/rake"
|
|
5
6
|
|
|
6
7
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
8
|
|
|
8
|
-
desc
|
|
9
|
+
desc "Console"
|
|
9
10
|
task :console do
|
|
10
|
-
require
|
|
11
|
-
require
|
|
12
|
-
require
|
|
13
|
-
|
|
11
|
+
require "bundler/setup"
|
|
12
|
+
require "iml"
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
task default:
|
|
17
|
+
task default: %i[spec standard]
|
data/TODO.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# TODO: IML Gem Comprehensive Refactor
|
|
2
|
+
|
|
3
|
+
## Phase 1: Modernize Dependencies and Tooling
|
|
4
|
+
- [x] Step 1.1: Update Ruby version requirement and gemspec
|
|
5
|
+
- [x] Step 1.2: Replace Travis CI with GitHub Actions
|
|
6
|
+
- [x] Step 1.3: Switch linting to StandardRB
|
|
7
|
+
- [x] Step 1.4: Update Gemfile and run bundle install
|
|
8
|
+
|
|
9
|
+
## Phase 2: Architecture Rewrite — Composition Over Inheritance
|
|
10
|
+
- [x] Step 2.1: Create Configuration module (replace IML::Hash)
|
|
11
|
+
- [x] Step 2.2: Rewrite Pattern Builder (remove method_missing)
|
|
12
|
+
- [x] Step 2.3: Create Media Result value objects (replace OpenStruct)
|
|
13
|
+
- [x] Step 2.4: Create Formatter service
|
|
14
|
+
- [x] Step 2.5: Create Normalizer service
|
|
15
|
+
- [x] Step 2.6: Rewrite Parser (replace IML::Text < String)
|
|
16
|
+
- [x] Step 2.7: Create File Mover service
|
|
17
|
+
- [x] Step 2.8: Update main entry point (lib/iml.rb)
|
|
18
|
+
- [x] Step 2.9: Delete legacy files
|
|
19
|
+
|
|
20
|
+
## Phase 3: CLI Modernization
|
|
21
|
+
- [x] Step 3.1: Rewrite CLI script (add --version, remove tqdm, proper exit codes)
|
|
22
|
+
|
|
23
|
+
## Phase 4: Remove ActiveSupport Dependency
|
|
24
|
+
- [x] Step 4.1: Replace ActiveSupport Inflector with lightweight titleize
|
|
25
|
+
- [x] Step 4.2: Remove HashWithIndifferentAccess usage
|
|
26
|
+
- [x] Step 4.3: Remove activesupport from gemspec
|
|
27
|
+
|
|
28
|
+
## Phase 5: Comprehensive Test Suite
|
|
29
|
+
- [x] Step 5.1: Set up test infrastructure (spec_helper, :verified tags)
|
|
30
|
+
- [x] Step 5.2: Test Configuration
|
|
31
|
+
- [x] Step 5.3: Test Pattern Builder
|
|
32
|
+
- [x] Step 5.4: Test Parser
|
|
33
|
+
- [x] Step 5.5: Test Media Result objects (Movie, TvSeries)
|
|
34
|
+
- [x] Step 5.6: Test Formatter
|
|
35
|
+
- [x] Step 5.7: Test Normalizer
|
|
36
|
+
- [x] Step 5.8: Test File Mover
|
|
37
|
+
- [x] Step 5.9: Delete legacy tests
|
|
38
|
+
|
|
39
|
+
## Phase 6: Final Cleanup and Verification
|
|
40
|
+
- [x] Step 6.1: Run StandardRB and fix issues
|
|
41
|
+
- [x] Step 6.2: Run full test suite
|
|
42
|
+
- [x] Step 6.3: Build and verify gem
|
|
43
|
+
- [x] Step 6.4: Update README
|
data/bin/iml
CHANGED
|
@@ -1,96 +1,106 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require
|
|
5
|
-
require
|
|
4
|
+
require "iml"
|
|
5
|
+
require "optparse"
|
|
6
6
|
|
|
7
7
|
options = {}
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
parser = OptionParser.new do |opts|
|
|
10
|
+
opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] MEDIA_FILE [MEDIA_FILE] ..."
|
|
11
|
+
|
|
12
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") { |v| options[:verbose] = v }
|
|
13
|
+
opts.on("-p", "--[no-]pretend", "Dry run, do not move any files") { |p| options[:pretend] = p }
|
|
14
|
+
opts.on("-t", "--target PATH", "Output directory (default: current directory)") { |t| options[:target] = t }
|
|
15
|
+
opts.on("-o", "--movie-format FORMAT", "Movie output format (default: '#{IML::Media::Movie::DEFAULT_FORMAT}')") { |f| options[:movie_format] = f }
|
|
16
|
+
opts.on("-O", "--tv-format FORMAT", "TV series output format (default: '#{IML::Media::TvSeries::DEFAULT_FORMAT}')") { |f| options[:tv_format] = f }
|
|
17
|
+
opts.on("-l", "--list-formats", "Show format placeholders") { options[:list_formats] = true }
|
|
18
|
+
opts.on("-f", "--force", "Overwrite existing output files") { options[:force] = true }
|
|
19
|
+
opts.on("-V", "--version", "Show version") do
|
|
20
|
+
puts "iml #{IML::VERSION}"
|
|
21
|
+
exit
|
|
22
|
+
end
|
|
23
|
+
end
|
|
9
24
|
|
|
10
25
|
begin
|
|
11
|
-
|
|
12
|
-
opts.banner = "Usage: #{File.basename $PROGRAM_NAME} [options] MEDIA_FILE [MEDIA_FILE] ..."
|
|
13
|
-
|
|
14
|
-
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
|
|
15
|
-
options[:verbose] = v
|
|
16
|
-
end
|
|
17
|
-
opts.on('-p', '--[no-]pretend', 'Dry run, do not move any files') do |p|
|
|
18
|
-
options[:pretend] = p
|
|
19
|
-
end
|
|
20
|
-
opts.on('-t', '--target PATH', 'Path to move media files to, default: current directory') do |t|
|
|
21
|
-
options[:target] = t
|
|
22
|
-
end
|
|
23
|
-
opts.on('-o', '--movie-format FORMAT', "Format of the output path of movies, default: '#{IML::Movie::DEFAULT_FORMAT}'") do |f|
|
|
24
|
-
options[:movie_format] = f
|
|
25
|
-
end
|
|
26
|
-
opts.on('-O', '--tv-format FORMAT', "Format of the output path of TV series, default: '#{IML::TVSeries::DEFAULT_FORMAT}'") do |f|
|
|
27
|
-
options[:tv_format] = f
|
|
28
|
-
end
|
|
29
|
-
opts.on('-l', '--list-formats', 'Format description') do |f|
|
|
30
|
-
options[:format] = f
|
|
31
|
-
end
|
|
32
|
-
opts.on('-f', '--force', 'Use the force, override output files') do |f|
|
|
33
|
-
options[:force] = f
|
|
34
|
-
end
|
|
35
|
-
puts opts if opts.default_argv.empty? && STDIN.tty?
|
|
36
|
-
end.parse!
|
|
26
|
+
parser.parse!
|
|
37
27
|
rescue OptionParser::InvalidOption => e
|
|
38
|
-
|
|
28
|
+
$stderr.puts e.message
|
|
29
|
+
exit 1
|
|
39
30
|
end
|
|
40
31
|
|
|
41
|
-
if
|
|
42
|
-
puts
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
%t - episode title
|
|
46
|
-
%Y - year
|
|
47
|
-
%S - season number string as found in the input path
|
|
48
|
-
%s - season number integer
|
|
49
|
-
%E - episode number string as found in the input path
|
|
50
|
-
%e - episode number integer
|
|
51
|
-
%g - group name
|
|
52
|
-
%a - audio codec
|
|
53
|
-
%v - video codec
|
|
54
|
-
%f - file format / extension
|
|
55
|
-
%z - source
|
|
56
|
-
%q - quality
|
|
32
|
+
if $stdin.tty? && ARGV.empty?
|
|
33
|
+
puts parser
|
|
34
|
+
exit
|
|
35
|
+
end
|
|
57
36
|
|
|
37
|
+
if options[:list_formats]
|
|
38
|
+
puts <<~FORMAT
|
|
39
|
+
Format placeholders:
|
|
40
|
+
%T - title
|
|
41
|
+
%t - episode title
|
|
42
|
+
%Y - year
|
|
43
|
+
%S - season number (string, e.g. "03")
|
|
44
|
+
%s - season number (integer, e.g. "3")
|
|
45
|
+
%E - episode number (string, e.g. "09")
|
|
46
|
+
%e - episode number (integer, e.g. "9")
|
|
47
|
+
%g - group name
|
|
48
|
+
%a - audio codec
|
|
49
|
+
%v - video codec
|
|
50
|
+
%f - file extension
|
|
51
|
+
%z - source
|
|
52
|
+
%q - quality
|
|
58
53
|
FORMAT
|
|
59
54
|
exit
|
|
60
55
|
end
|
|
61
56
|
|
|
62
|
-
input =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
dirname = pathname.dirname
|
|
69
|
-
unless dirname.to_s == '.'
|
|
70
|
-
@logger.info "Creating #{dirname}" if options[:verbose] || options[:pretend]
|
|
71
|
-
media.create_dir
|
|
72
|
-
end
|
|
73
|
-
@logger.info "Moving #{path} to #{pathname}" if options[:verbose] || options[:pretend]
|
|
74
|
-
media.move(path)
|
|
57
|
+
input = if ARGV.any?
|
|
58
|
+
ARGV
|
|
59
|
+
elsif !$stdin.tty?
|
|
60
|
+
$stdin.readlines
|
|
61
|
+
else
|
|
62
|
+
[]
|
|
75
63
|
end
|
|
64
|
+
media_parser = IML::Parser.new
|
|
65
|
+
formatter = IML::Formatter.new
|
|
66
|
+
mover = IML::FileMover.new(formatter: formatter)
|
|
67
|
+
|
|
68
|
+
processed = 0
|
|
69
|
+
skipped = 0
|
|
76
70
|
|
|
77
|
-
input.
|
|
71
|
+
input.each do |entry|
|
|
72
|
+
path = Pathname(entry.chomp)
|
|
78
73
|
filename = path.basename.to_s
|
|
74
|
+
|
|
79
75
|
unless path.exist?
|
|
80
|
-
|
|
76
|
+
$stderr.puts "skip: #{path} (file not found)"
|
|
77
|
+
skipped += 1
|
|
81
78
|
next
|
|
82
79
|
end
|
|
83
|
-
|
|
80
|
+
|
|
81
|
+
media = media_parser.parse(filename)
|
|
84
82
|
unless media
|
|
85
|
-
|
|
83
|
+
$stderr.puts "skip: #{path} (not recognized)" if options[:verbose] || options[:pretend]
|
|
84
|
+
skipped += 1
|
|
86
85
|
next
|
|
87
86
|
end
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
|
|
88
|
+
format = media.movie? ? options[:movie_format] : options[:tv_format]
|
|
89
|
+
dest = formatter.pathname(media, format: format, target: options[:target])
|
|
90
|
+
type_label = media.movie? ? "movie" : "tv"
|
|
91
|
+
|
|
92
|
+
if options[:verbose] || options[:pretend]
|
|
93
|
+
$stderr.puts "#{type_label}: #{path} -> #{dest}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
unless options[:pretend]
|
|
97
|
+
mover.call(path, media, format: format, target: options[:target])
|
|
94
98
|
end
|
|
95
|
-
|
|
99
|
+
|
|
100
|
+
processed += 1
|
|
101
|
+
rescue IML::FileNotFoundError => e
|
|
102
|
+
$stderr.puts "error: #{path} (#{e.message})"
|
|
103
|
+
skipped += 1
|
|
96
104
|
end
|
|
105
|
+
|
|
106
|
+
$stderr.puts "done: #{processed} processed, #{skipped} skipped" if options[:verbose] || options[:pretend]
|
data/gemspec.yml
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
dependencies:
|
|
2
|
-
activesupport: ~> 5.2
|
|
3
|
-
tqdm: ~> 0.3
|
|
1
|
+
dependencies: {}
|
|
4
2
|
|
|
5
3
|
development_dependencies:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
yard: ~> 0.9
|
|
4
|
+
bundler: '>= 2.4'
|
|
5
|
+
rake: '~> 13.0'
|
|
6
|
+
rspec: '~> 3.13'
|
|
7
|
+
simplecov: '~> 0.22'
|
|
8
|
+
standard: '~> 1.40'
|
|
9
|
+
yard: '~> 0.9'
|
data/iml.gemspec
CHANGED
|
@@ -1,48 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
lib = File.expand_path(
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
-
require
|
|
6
|
-
require
|
|
5
|
+
require "iml/version"
|
|
6
|
+
require "yaml"
|
|
7
7
|
|
|
8
8
|
Gem::Specification.new do |spec|
|
|
9
|
-
gemspec = YAML.
|
|
10
|
-
spec.name
|
|
11
|
-
spec.version
|
|
12
|
-
spec.authors
|
|
13
|
-
spec.email
|
|
14
|
-
|
|
15
|
-
spec.summary
|
|
16
|
-
spec.description
|
|
17
|
-
spec.homepage
|
|
18
|
-
spec.license
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if spec.respond_to?(:metadata)
|
|
23
|
-
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
24
|
-
else
|
|
25
|
-
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
|
26
|
-
'public gem pushes.'
|
|
27
|
-
end
|
|
9
|
+
gemspec = YAML.safe_load_file("gemspec.yml")
|
|
10
|
+
spec.name = "iml"
|
|
11
|
+
spec.version = IML::VERSION
|
|
12
|
+
spec.authors = ["Adam Ladachowski"]
|
|
13
|
+
spec.email = ["adam.ladachowski@gmail.com"]
|
|
14
|
+
|
|
15
|
+
spec.summary = "Media string and object manipulation library"
|
|
16
|
+
spec.description = "Library which parses strings into media objects"
|
|
17
|
+
spec.homepage = "https://github.com/aladac/iml"
|
|
18
|
+
spec.license = "MIT"
|
|
19
|
+
|
|
20
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
21
|
+
spec.metadata["yard.run"] = "yri"
|
|
28
22
|
|
|
29
|
-
# Specify which files should be added to the gem when it is released.
|
|
30
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
31
23
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
32
24
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
33
25
|
end
|
|
34
|
-
spec.bindir
|
|
35
|
-
spec.executables
|
|
36
|
-
spec.require_paths = [
|
|
26
|
+
spec.bindir = "bin"
|
|
27
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
28
|
+
spec.require_paths = ["lib"]
|
|
37
29
|
|
|
38
|
-
spec.required_ruby_version =
|
|
39
|
-
spec.metadata['yard.run'] = 'yri'
|
|
30
|
+
spec.required_ruby_version = ">= 3.1"
|
|
40
31
|
|
|
41
|
-
gemspec[
|
|
32
|
+
gemspec["dependencies"].each do |name, version|
|
|
42
33
|
spec.add_dependency(name, version)
|
|
43
34
|
end
|
|
44
35
|
|
|
45
|
-
gemspec[
|
|
36
|
+
gemspec["development_dependencies"].each do |name, version|
|
|
46
37
|
spec.add_development_dependency(name, version)
|
|
47
38
|
end
|
|
48
39
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class IML::Configuration
|
|
4
|
+
attr_reader :data
|
|
5
|
+
|
|
6
|
+
def initialize(path = nil)
|
|
7
|
+
path ||= default_path
|
|
8
|
+
@data = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def codec_map
|
|
12
|
+
@codec_map ||= data.fetch("codec", {})
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def audio_map
|
|
16
|
+
@audio_map ||= data.fetch("audio", {})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def source_map
|
|
20
|
+
@source_map ||= data.fetch("source", {})
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def quality_list
|
|
24
|
+
@quality_list ||= data.fetch("quality", [])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def extension_list
|
|
28
|
+
@extension_list ||= data.fetch("extension", [])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def pattern_for(field)
|
|
32
|
+
value = data.fetch(field.to_s)
|
|
33
|
+
|
|
34
|
+
case value
|
|
35
|
+
when Hash
|
|
36
|
+
"(?<#{field}>(#{value.keys.join("|")}))"
|
|
37
|
+
when Array
|
|
38
|
+
"(?<#{field}>(#{value.join("|")}))"
|
|
39
|
+
when String
|
|
40
|
+
"(?<#{field}>#{value})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def default_path
|
|
47
|
+
File.expand_path("../../patterns.yml", __dir__)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class IML::FileMover
|
|
4
|
+
def initialize(formatter: IML::Formatter.new)
|
|
5
|
+
@formatter = formatter
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(source_path, media, format: nil, target: nil, pretend: false)
|
|
9
|
+
dest = @formatter.pathname(media, format: format, target: target)
|
|
10
|
+
|
|
11
|
+
unless pretend
|
|
12
|
+
FileUtils.mkdir_p(dest.dirname)
|
|
13
|
+
FileUtils.mv(source_path.to_s, dest.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
dest
|
|
17
|
+
rescue Errno::ENOENT => e
|
|
18
|
+
raise IML::FileNotFoundError, e.message
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class IML::Formatter
|
|
4
|
+
def call(media, format: nil)
|
|
5
|
+
format_string = format || media.class::DEFAULT_FORMAT
|
|
6
|
+
media.class::PLACEHOLDERS.each do |placeholder, attribute|
|
|
7
|
+
format_string = format_string.gsub(placeholder, media.send(attribute).to_s)
|
|
8
|
+
end
|
|
9
|
+
format_string
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pathname(media, format: nil, target: nil)
|
|
13
|
+
formatted = call(media, format: format)
|
|
14
|
+
target ? Pathname(target) + Pathname(formatted) : Pathname(formatted)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class IML::Media::Movie
|
|
4
|
+
ATTRIBUTES = %i[title year quality source codec audio channels group extension].freeze
|
|
5
|
+
|
|
6
|
+
PLACEHOLDERS = {
|
|
7
|
+
"%T" => :title,
|
|
8
|
+
"%Y" => :year,
|
|
9
|
+
"%f" => :extension,
|
|
10
|
+
"%v" => :codec,
|
|
11
|
+
"%a" => :audio,
|
|
12
|
+
"%g" => :group,
|
|
13
|
+
"%z" => :source,
|
|
14
|
+
"%q" => :quality
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
DEFAULT_FORMAT = "%T (%Y).%f"
|
|
18
|
+
|
|
19
|
+
attr_reader(*ATTRIBUTES)
|
|
20
|
+
|
|
21
|
+
def initialize(**attrs)
|
|
22
|
+
ATTRIBUTES.each { |a| instance_variable_set(:"@#{a}", attrs[a]) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def movie? = true
|
|
26
|
+
|
|
27
|
+
def tv? = false
|
|
28
|
+
|
|
29
|
+
def type = :movie
|
|
30
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class IML::Media::TvSeries
|
|
4
|
+
ATTRIBUTES = %i[
|
|
5
|
+
title season episode episode_title quality source
|
|
6
|
+
codec audio channels group extension
|
|
7
|
+
].freeze
|
|
8
|
+
|
|
9
|
+
PLACEHOLDERS = {
|
|
10
|
+
"%T" => :title,
|
|
11
|
+
"%E" => :episode,
|
|
12
|
+
"%S" => :season,
|
|
13
|
+
"%f" => :extension,
|
|
14
|
+
"%e" => :episode_i,
|
|
15
|
+
"%s" => :season_i,
|
|
16
|
+
"%t" => :episode_title,
|
|
17
|
+
"%a" => :audio,
|
|
18
|
+
"%v" => :codec,
|
|
19
|
+
"%q" => :quality,
|
|
20
|
+
"%g" => :group,
|
|
21
|
+
"%z" => :source
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
DEFAULT_FORMAT = "%T/Season %s/%T - S%SE%E.%f"
|
|
25
|
+
|
|
26
|
+
attr_reader(*ATTRIBUTES)
|
|
27
|
+
|
|
28
|
+
def initialize(**attrs)
|
|
29
|
+
ATTRIBUTES.each { |a| instance_variable_set(:"@#{a}", attrs[a]) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def movie? = false
|
|
33
|
+
|
|
34
|
+
def tv? = true
|
|
35
|
+
|
|
36
|
+
def type = :tv
|
|
37
|
+
|
|
38
|
+
def season_i
|
|
39
|
+
season.to_i
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def episode_i
|
|
43
|
+
episode.to_i
|
|
44
|
+
end
|
|
45
|
+
end
|