iml 0.1.5 → 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.
data/README.md CHANGED
@@ -1,25 +1,23 @@
1
1
  [![Gem Version](https://img.shields.io/gem/v/iml.svg)](https://rubygems.org/gems/iml)
2
- [![build status](https://travis-ci.org/aladac/iml.svg?branch=master)](https://travis-ci.org/aladac/iml)
3
- [![Maintainability](https://api.codeclimate.com/v1/badges/232800c6e4d8778937b2/maintainability)](https://codeclimate.com/github/aladac/iml/maintainability)
4
- [![Test Coverage](https://api.codeclimate.com/v1/badges/232800c6e4d8778937b2/test_coverage)](https://codeclimate.com/github/aladac/iml/test_coverage)
5
- [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/github/aladac/iml/master)
6
2
 
7
3
  [![IML](https://github.com/aladac/iml/raw/master/doc/iml-logo.png)](https://rubygems.org/gems/iml)
8
4
 
9
5
  *Intricate (Media) Matching Logic*
10
6
 
11
- This is a media file handling library which is supposed to "guess" the intended type of media file based on specific naming patterns.
12
- Its main purpose is to serve as runtime for renaming media files according to specified patterns.
13
- The gem includes an executable `iml` through which rename operations are possible.
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
- This gem requires ruby >= 2.4
15
+ Requires Ruby >= 3.1
18
16
 
19
17
  Add this line to your application's Gemfile:
20
18
 
21
19
  ```ruby
22
- gem 'iml'
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 Path to move media files to, default: current directory
42
- -o, --movie-format FORMAT Format of the output path of movies, default: '%T (%Y).%f'
43
- -O, --tv-format FORMAT Format of the output path of TV series, default: '%T/Season %s/%T - S%SE%E.%f'
44
- -l, --list-formats Format description
45
- -f, --force Use the force, override output files
46
-
47
- $ iml -v Some.Cool.Movie.2018.1080p.BRRip.x264.aac-GROUP.mp4
48
- I, [2018-07-06T13:38:29.836887 #70771] INFO -- : Some.Cool.Movie.2018.1080p.BRRip.x264.aac-GROUP.mp4 looks like a movie
49
- I, [2018-07-06T13:38:29.837047 #70771] INFO -- : Moving Some.Cool.Movie.2018.1080p.BRRip.x264.aac-GROUP.mp4 to Some Cool Movie (2018).mp4
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
- ### Code
51
+ ### Library
53
52
 
54
53
  ```ruby
55
- title = "An.Interesting.TV.Show.S01E01.1080p.WEBRIP.h265-GROUP.mkv"
56
- => "An.Interesting.TV.Show.S01E01.1080p.WEBRIP.h265-GROUP.mkv"
57
- IML::Text.new(title).detect
58
- => #<IML::TVSeries title="An Interesting Tv Show", season="01", episode="01", quality="1080p", source="WEBRIP", codec="h265", group="GROUP", extension="mkv">
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 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
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 'Console'
9
+ desc "Console"
9
10
  task :console do
10
- require 'bundler/setup'
11
- require 'iml'
12
- require 'pry'
13
- Pry.start
11
+ require "bundler/setup"
12
+ require "iml"
13
+ require "irb"
14
+ IRB.start
14
15
  end
15
16
 
16
- task default: :spec
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 'iml'
5
- require 'optparse'
4
+ require "iml"
5
+ require "optparse"
6
6
 
7
7
  options = {}
8
- @logger = Logger.new(STDOUT)
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
- OptionParser.new do |opts|
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
- @logger.error e.message
28
+ $stderr.puts e.message
29
+ exit 1
39
30
  end
40
31
 
41
- if options[:format]
42
- puts <<~FORMAT
43
- Formatting description:
44
- %T - title
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 = STDIN.tty? ? ARGV : $stdin.readlines
63
-
64
- input ||= ARGV
65
-
66
- def file_operations(path, media, options)
67
- pathname = media.pathname
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.map { |p| Pathname(p.chomp) }.tqdm(desc: 'Processing input', leave: true).each do |path|
71
+ input.each do |entry|
72
+ path = Pathname(entry.chomp)
78
73
  filename = path.basename.to_s
74
+
79
75
  unless path.exist?
80
- @logger.warn("File #{path} does not exist, skipping")
76
+ $stderr.puts "skip: #{path} (file not found)"
77
+ skipped += 1
81
78
  next
82
79
  end
83
- media = IML::Text.new(filename, options).detect
80
+
81
+ media = media_parser.parse(filename)
84
82
  unless media
85
- @logger.warn "#{path} doesn't look like a media file" if options[:verbose] || options[:pretend]
83
+ $stderr.puts "skip: #{path} (not recognized)" if options[:verbose] || options[:pretend]
84
+ skipped += 1
86
85
  next
87
86
  end
88
- if media.movie?
89
- media.format_string = options[:movie_format] if options[:movie_format]
90
- @logger.info "#{path} looks like a movie"
91
- elsif media.tv?
92
- media.format_string = options[:tv_format] if options[:tv_format]
93
- @logger.info "#{path} looks like a TV series"
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
- file_operations(path, media, options)
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,15 +1,9 @@
1
- dependencies:
2
- activesupport: ~> 5.2
3
- tqdm: ~> 0.3
4
- nokogiri: ~> 1.8
1
+ dependencies: {}
5
2
 
6
3
  development_dependencies:
7
- codeclimate-test-reporter: ~> 1.0
8
- bundler: ~> 1.16
9
- rake: ~> 12.3
10
- rspec: ~> 3.7
11
- pry: ~> 0.11
12
- simplecov: ~> 0.16
13
- yard: ~> 0.9
14
- vcr: ~> 4.0
15
- webmock: ~> 3.4
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('lib', __dir__)
3
+ lib = File.expand_path("lib", __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'iml/version'
6
- require 'yaml'
5
+ require "iml/version"
6
+ require "yaml"
7
7
 
8
8
  Gem::Specification.new do |spec|
9
- gemspec = YAML.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
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
21
- # to allow pushing to a single host or delete this section to allow pushing to any host.
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 = 'bin'
35
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
36
- spec.require_paths = ['lib']
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 = '>= 2.4'
39
- spec.metadata['yard.run'] = 'yri'
30
+ spec.required_ruby_version = ">= 3.1"
40
31
 
41
- gemspec['dependencies'].each do |name, version|
32
+ gemspec["dependencies"].each do |name, version|
42
33
  spec.add_dependency(name, version)
43
34
  end
44
35
 
45
- gemspec['development_dependencies'].each do |name, version|
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