episode 1.0.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/LICENSE.md +21 -0
- data/README.md +121 -0
- data/bin/ep +92 -0
- data/episode.gemspec +31 -0
- data/lib/episode.rb +277 -0
- data/lib/episode/config.rb +133 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e4f2ae42c41aa916a30370e05e9150baa621cbdffcf1e668f8afc81889f91af8
|
|
4
|
+
data.tar.gz: 2abf7c83c530be2d76497a682e5605370fb57cec86fe066246a840e934ead5ee
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 83caee9209e83b7b02a1085bcb33e6f795652436b2470af80366d5b12a231e25bf69194c43c3f4e09b2bdf72d45f7439a6edda6f26f9e135ef8c2db8d01f3493
|
|
7
|
+
data.tar.gz: 483aa5505b1fd3db50b94cdc198f120e394c0fde02675d29df1666a4410ae4a5d193a21e8b36c0eeb8508051905664cfce0a9dbaa6ecf45c1f43bbde2496542d
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Maksim Esterkin
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Episode (ep)
|
|
2
|
+
Remembers what file in the directory you viewed last time.
|
|
3
|
+
Enumerates all files in the directory and allows to reference them only by a number.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
#### Using `gem`
|
|
8
|
+
```
|
|
9
|
+
gem install episode
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
#### Manual
|
|
13
|
+
Clone this repository and add `bin/ep` to your `$PATH`.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
Episode creates `.episode` file in the current directory when you open some file with it. By default it will be looking for `mkv`, `avi` or `mp4` files and will use `mpv` as the viewer. Read [examples](#viewing-different-file-formats) below to see how to change this behavior.
|
|
18
|
+
|
|
19
|
+
List episodes in the current directory.
|
|
20
|
+
```
|
|
21
|
+
ep
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Watch episode #7
|
|
25
|
+
```
|
|
26
|
+
ep 7
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Watch with VLC
|
|
30
|
+
```
|
|
31
|
+
ep 7 -v vlc
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Make VLC default player for this directory (add `-g` to make it global)
|
|
35
|
+
```
|
|
36
|
+
ep set viewer vlc
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Show config for the current directory
|
|
40
|
+
```
|
|
41
|
+
ep cfg
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Show global config
|
|
45
|
+
```
|
|
46
|
+
ep cfg -g
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage (`ep help`)
|
|
50
|
+
```
|
|
51
|
+
Usage: ep <command> [options]
|
|
52
|
+
|
|
53
|
+
Quick start:
|
|
54
|
+
ep ls show episodes in the current directory with their indexes
|
|
55
|
+
ep same as `ep ls`
|
|
56
|
+
ep 7 play episode #7
|
|
57
|
+
ep next play next episode (or first)
|
|
58
|
+
ep set viewer vlc -g use VLC as default video player (by default it's mpv)
|
|
59
|
+
|
|
60
|
+
Commands:
|
|
61
|
+
ls List all episodes and their idnexes
|
|
62
|
+
(s) status Show information about last view
|
|
63
|
+
(l) last Re-play episode watched last time
|
|
64
|
+
(n) next Play next episode
|
|
65
|
+
(p) prev Play previous episode (one before 'last')
|
|
66
|
+
<number> Same as `ep no <number>` (i.e. `ep 11`)
|
|
67
|
+
no <number> Play episode by number (i.e. `ep no 11`)
|
|
68
|
+
(c) cfg Display config for the current directory
|
|
69
|
+
set <param> <value> Set config parameter (i.e. `ep set last 11`)
|
|
70
|
+
(r) reset [param] Reset config parameter (i.e. `ep reset last`)
|
|
71
|
+
(h) help Show this help
|
|
72
|
+
|
|
73
|
+
Options for `last`, `next`, `prev`, and `no`:
|
|
74
|
+
-n, --name Show episode name, but don't play it (i.e. `ep -n 11`)
|
|
75
|
+
-o, --no-update Don't update .episode file
|
|
76
|
+
-v, --viewer <program> Set viewer
|
|
77
|
+
|
|
78
|
+
Options for `cfg`, `set` and `reset`:
|
|
79
|
+
-g, --global Edit (or show) global config ($HOME/.config/episode)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## More Examples
|
|
83
|
+
|
|
84
|
+
#### Coloring the 'last' pointer
|
|
85
|
+
```
|
|
86
|
+
ep -g set pointer '\u001b[33;1m*\u001b[0m'
|
|
87
|
+
```
|
|
88
|
+
<img src="https://static.hedlx.org/episode_coloring_pointer.png">
|
|
89
|
+
|
|
90
|
+
#### Purging all `episode` data from the directory
|
|
91
|
+
```
|
|
92
|
+
ep r
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Restoring global default settings
|
|
96
|
+
```
|
|
97
|
+
ep r -g
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Viewing different file formats
|
|
101
|
+
|
|
102
|
+
###### Pictures
|
|
103
|
+
```
|
|
104
|
+
ep set formats png,jpg
|
|
105
|
+
ep set viewer feh
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
###### PDF
|
|
109
|
+
```
|
|
110
|
+
ep set formats pdf
|
|
111
|
+
ep set viewer zathura
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
###### Any
|
|
115
|
+
```
|
|
116
|
+
ep set formats ''
|
|
117
|
+
ep set viewer 'hexdump -C'
|
|
118
|
+
|
|
119
|
+
# Invoking:
|
|
120
|
+
ep next | less
|
|
121
|
+
```
|
data/bin/ep
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require_relative '../lib/episode'
|
|
5
|
+
|
|
6
|
+
PROGRAM_NAME = File.basename($PROGRAM_NAME)
|
|
7
|
+
CFG_FILENAME = '.episode'
|
|
8
|
+
CFG_GLOBAL_DIR = ENV['XDG_CONFIG_HOME'] || File.join(ENV['HOME'], '.config')
|
|
9
|
+
CFG_GLOBAL_PATH = File.join(CFG_GLOBAL_DIR, 'episode')
|
|
10
|
+
|
|
11
|
+
options = {}
|
|
12
|
+
opt_parser =
|
|
13
|
+
OptionParser.new do |opts|
|
|
14
|
+
opts.program_name = 'episode'
|
|
15
|
+
opts.version = "v#{Episode::VERSION}"
|
|
16
|
+
opts.banner = <<~EOS
|
|
17
|
+
Usage: #{PROGRAM_NAME} <command> [options]
|
|
18
|
+
|
|
19
|
+
Quick start:
|
|
20
|
+
ep ls show episodes in the current directory with their indexes
|
|
21
|
+
ep same as `ep ls`
|
|
22
|
+
ep 7 play episode #7
|
|
23
|
+
ep next play next episode (or first)
|
|
24
|
+
ep set viewer vlc -g use VLC as default video player (by default it's mpv)
|
|
25
|
+
|
|
26
|
+
Commands:
|
|
27
|
+
ls List all episodes and their idnexes
|
|
28
|
+
(s) status Show information about last view
|
|
29
|
+
(l) last Re-play episode watched last time
|
|
30
|
+
(n) next Play next episode
|
|
31
|
+
(p) prev Play previous episode (one before 'last')
|
|
32
|
+
<number> Same as `#{PROGRAM_NAME} no <number>` (i.e. `#{PROGRAM_NAME} 11`)
|
|
33
|
+
no <number> Play episode by number (i.e. `#{PROGRAM_NAME} no 11`)
|
|
34
|
+
(c) cfg Display config for the current directory
|
|
35
|
+
set <param> <value> Set config parameter (i.e. `#{PROGRAM_NAME} set last 11`)
|
|
36
|
+
(r) reset [param] Reset config parameter (i.e. `#{PROGRAM_NAME} reset last`)
|
|
37
|
+
(h) help Show this help
|
|
38
|
+
EOS
|
|
39
|
+
|
|
40
|
+
opts.separator("\n Options for `last`, `next`, `prev`, and `no`:")
|
|
41
|
+
|
|
42
|
+
opts.on('-n', '--name', "Show episode name, but don't play it (i.e. `#{PROGRAM_NAME} -n 11`)") do
|
|
43
|
+
options[:name] = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.on('-o', '--no-update', "Don't update .episode file") do
|
|
47
|
+
options[:update] = false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
opts.on('-v', '--viewer <program>', 'Set viewer') do |viewer|
|
|
51
|
+
options[:viewer] = viewer
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
opts.separator("\n Options for `cfg`, `set` and `reset`:")
|
|
55
|
+
|
|
56
|
+
opts.on('-g', '--global', "Edit (or show) global config (#{CFG_GLOBAL_PATH})") do
|
|
57
|
+
options[:global] = true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
opt_parser.parse!
|
|
63
|
+
|
|
64
|
+
case
|
|
65
|
+
when ARGV[0].nil?
|
|
66
|
+
command = 'ls'
|
|
67
|
+
args = []
|
|
68
|
+
when %w[h help].include?(ARGV[0])
|
|
69
|
+
$stderr.puts opt_parser.help
|
|
70
|
+
return
|
|
71
|
+
when ARGV[0] =~ /^\d+$/
|
|
72
|
+
command = 'no'
|
|
73
|
+
args = [ARGV[0]]
|
|
74
|
+
else
|
|
75
|
+
command = ARGV[0]
|
|
76
|
+
args = ARGV[1..]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Episode.new(options).public_send(command, *args)
|
|
80
|
+
rescue OptionParser::ParseError => e
|
|
81
|
+
$stderr.puts <<~EOS
|
|
82
|
+
Error: #{e.message}
|
|
83
|
+
Run `#{PROGRAM_NAME} -h` to see the list of available options.
|
|
84
|
+
EOS
|
|
85
|
+
rescue Episode::NoCommandError => e
|
|
86
|
+
$stderr.puts <<~EOS
|
|
87
|
+
Error: Unknown command '#{e.message}'.
|
|
88
|
+
Run `#{PROGRAM_NAME} -h` to see the list of available commands.
|
|
89
|
+
EOS
|
|
90
|
+
rescue Episode::CommandError => e
|
|
91
|
+
$stderr.puts "Error: #{e.message}"
|
|
92
|
+
end
|
data/episode.gemspec
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative "lib/episode"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = 'episode'
|
|
5
|
+
s.version = "#{Episode::VERSION}.0.0"
|
|
6
|
+
s.summary = 'Console assistant app for watching series'
|
|
7
|
+
s.license = 'MIT'
|
|
8
|
+
s.description = 'Episode (ep) remembers what file in the directory you viewed last time. Enumerates all files in the directory and allows to reference them only by a number.'
|
|
9
|
+
s.authors = ['Maksim Esterkin']
|
|
10
|
+
s.email = 'esterkimx@gmail.com'
|
|
11
|
+
s.homepage = 'https://github.com/esterkimx/episode'
|
|
12
|
+
|
|
13
|
+
s.files = [
|
|
14
|
+
'lib/episode.rb',
|
|
15
|
+
'lib/episode/config.rb',
|
|
16
|
+
'README.md',
|
|
17
|
+
'LICENSE.md',
|
|
18
|
+
'episode.gemspec'
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
s.required_ruby_version = '>= 2.3.0'
|
|
22
|
+
s.bindir = 'bin'
|
|
23
|
+
s.executables = ['ep']
|
|
24
|
+
|
|
25
|
+
s.metadata = {
|
|
26
|
+
'bug_tracker_uri' => 'https://github.com/esterkimx/episode/issues',
|
|
27
|
+
'documentation_uri' => 'https://github.com/esterkimx/episode/blob/master/README.md',
|
|
28
|
+
'homepage_uri' => 'https://github.com/esterkimx/episode',
|
|
29
|
+
'source_code_uri' => 'https://github.com/esterkimx/episode'
|
|
30
|
+
}
|
|
31
|
+
end
|
data/lib/episode.rb
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'time'
|
|
3
|
+
require 'io/console'
|
|
4
|
+
require_relative 'episode/config'
|
|
5
|
+
|
|
6
|
+
class Episode
|
|
7
|
+
VERSION = 1
|
|
8
|
+
|
|
9
|
+
class NoCommandError < StandardError; end
|
|
10
|
+
class CommandError < StandardError; end
|
|
11
|
+
|
|
12
|
+
def initialize(opts)
|
|
13
|
+
@is_name = opts[:name] || false
|
|
14
|
+
@is_update = opts[:update] != false
|
|
15
|
+
@is_global = opts[:global] || false
|
|
16
|
+
@viewer = opts[:viewer]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ls
|
|
20
|
+
if episodes.empty?
|
|
21
|
+
raise CommandError, 'No episodes found in the directory'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
total = episodes.size
|
|
25
|
+
padding = Math.log10(config.index_from_zero ? total - 1 : total).floor + 1
|
|
26
|
+
|
|
27
|
+
episodes.each_with_index do |filename, id|
|
|
28
|
+
id_fixed = config.index_from_zero ? id : id + 1
|
|
29
|
+
id_formatted = id_fixed.to_s.rjust(padding, '0')
|
|
30
|
+
separator = (config.last && id == last_id) ? config.pointer : '|'
|
|
31
|
+
puts "#{id_formatted} #{separator} #{filename}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def status
|
|
36
|
+
puts "#{config.index_from_zero ? last_id : last_id + 1} #{config.pointer} #{config.last}"
|
|
37
|
+
|
|
38
|
+
unless config.last_played_at
|
|
39
|
+
puts 'Time unknown'
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
seconds_ago = (Time.now - config.last_played_at).floor
|
|
44
|
+
days_ago = seconds_ago / (3600 * 24)
|
|
45
|
+
hours_ago = (seconds_ago % (3600 * 24)) / 3600
|
|
46
|
+
minutes_ago = (seconds_ago % 3600) / 60
|
|
47
|
+
time_passed =
|
|
48
|
+
if seconds_ago < 60
|
|
49
|
+
"#{seconds_ago} second#{seconds_ago == 1 ? '' : 's'}"
|
|
50
|
+
else
|
|
51
|
+
[
|
|
52
|
+
days_ago > 0 ? "#{days_ago} day#{days_ago == 1 ? '' : 's'}" : nil,
|
|
53
|
+
hours_ago > 0 ? "#{hours_ago} hour#{hours_ago == 1 ? '' : 's'}" : nil,
|
|
54
|
+
minutes_ago > 0 ? "#{minutes_ago} minute#{minutes_ago == 1 ? '' : 's'}" : nil
|
|
55
|
+
].compact.join(" ")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "#{config.last_played_at} (#{time_passed} ago)"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
alias s status
|
|
62
|
+
|
|
63
|
+
def last
|
|
64
|
+
play
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
alias l last
|
|
68
|
+
|
|
69
|
+
def next
|
|
70
|
+
config.last = config.last ? episode_by_id(last_id + 1) : episodes.first
|
|
71
|
+
play
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
alias n next
|
|
75
|
+
|
|
76
|
+
def prev
|
|
77
|
+
config.last = episode_by_id(last_id - 1)
|
|
78
|
+
play
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
alias p prev
|
|
82
|
+
|
|
83
|
+
def no(n_str)
|
|
84
|
+
n =
|
|
85
|
+
begin
|
|
86
|
+
Integer(n_str.gsub /^0*(\d)/, '\\1')
|
|
87
|
+
rescue ArgumentError
|
|
88
|
+
raise CommandError, <<~EOS
|
|
89
|
+
Invalid value '#{n_str}'.
|
|
90
|
+
`no` command expects natural number.
|
|
91
|
+
EOS
|
|
92
|
+
end
|
|
93
|
+
config.last = episode_by_id(n - 1)
|
|
94
|
+
play
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def cfg
|
|
98
|
+
cfg_h = global? ? config.global.to_h : config.to_h
|
|
99
|
+
cfg_h.each { |param, val| puts "#{param}: #{val}" }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
alias c cfg
|
|
103
|
+
|
|
104
|
+
def set(param, value)
|
|
105
|
+
unless config.respond_to? param
|
|
106
|
+
raise CommandError, <<~EOS
|
|
107
|
+
Unknown config parameter '#{param}'.
|
|
108
|
+
Run `#{PROGRAM_NAME} cfg` to see the list of available parameters.
|
|
109
|
+
EOS
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
cfg, cfg_path =
|
|
113
|
+
if global?
|
|
114
|
+
[config.global, CFG_GLOBAL_PATH]
|
|
115
|
+
else
|
|
116
|
+
[config, CFG_FILENAME]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
cfg.send "#{param}=", parse_config_value(param, value)
|
|
120
|
+
config_save_safe(cfg_path, cfg)
|
|
121
|
+
rescue NotLocal
|
|
122
|
+
raise CommandError, "Parameter '#{param}' is not global"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def reset(param = nil)
|
|
126
|
+
cfg_path = global? ? CFG_GLOBAL_PATH : CFG_FILENAME
|
|
127
|
+
|
|
128
|
+
if param.nil?
|
|
129
|
+
puts "Reset all config parameters (delete #{cfg_path})? (y|N)"
|
|
130
|
+
FileUtils.rm_f(cfg_path) if 'y' == $stdin.getch
|
|
131
|
+
else
|
|
132
|
+
set(param, nil)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
alias r reset
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def method_missing(*args)
|
|
141
|
+
raise NoCommandError, args.first
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def name?
|
|
145
|
+
@is_name
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def update?
|
|
149
|
+
@is_update
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def global?
|
|
153
|
+
@is_global
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def config
|
|
157
|
+
return @config if @config
|
|
158
|
+
|
|
159
|
+
global_cfg =
|
|
160
|
+
if File.exists? CFG_GLOBAL_PATH
|
|
161
|
+
File.open(CFG_GLOBAL_PATH, 'r') { |io| Config.load io }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
@config =
|
|
165
|
+
if File.exists? CFG_FILENAME
|
|
166
|
+
File.open(CFG_FILENAME, 'r') { |io| Config.load io, global: global_cfg }
|
|
167
|
+
else
|
|
168
|
+
Config.new(global: global_cfg)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def viewer
|
|
173
|
+
@viewer || config.viewer
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def episodes
|
|
177
|
+
@episodes ||=
|
|
178
|
+
Dir["./*{#{config.formats.join(',')}}"]
|
|
179
|
+
.select { |path| File.file? path }
|
|
180
|
+
.map { |path| File.basename(path) }.sort
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def episode_by_id(id)
|
|
184
|
+
ep = episodes[id] if id >= 0
|
|
185
|
+
ep || raise(CommandError, 'Episode not found')
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def parse_episode_ref(ref)
|
|
189
|
+
case
|
|
190
|
+
when File.file?(ref)
|
|
191
|
+
ref
|
|
192
|
+
when ref =~ /^\d+$/
|
|
193
|
+
ref_i = ref.to_i
|
|
194
|
+
id = config.index_from_zero ? ref_i : ref_i - 1
|
|
195
|
+
episode_by_id(id)
|
|
196
|
+
else
|
|
197
|
+
raise CommandError, "Can't parse episode reference '#{ref}'"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def last_safe
|
|
202
|
+
unless config.last
|
|
203
|
+
raise CommandError, <<~EOS
|
|
204
|
+
Last episode is undefined.
|
|
205
|
+
Please run:
|
|
206
|
+
`#{PROGRAM_NAME} #{config.index_from_zero ? 0 : 1}` or `ep next` -- to watch first episode
|
|
207
|
+
`#{PROGRAM_NAME} set last <episode-number>` or `#{PROGRAM_NAME} set last <file-name>` -- to define where to start from
|
|
208
|
+
EOS
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
unless File.file? config.last
|
|
212
|
+
raise CommandError, <<~EOS
|
|
213
|
+
'#{config.last}' doesn't exist.
|
|
214
|
+
To fix it run:
|
|
215
|
+
`#{PROGRAM_NAME} set last <file-name>` or `#{PROGRAM_NAME} set last <episode-number>` -- to define last file
|
|
216
|
+
`#{PROGRAM_NAME} reset last` -- to erase erroneous value
|
|
217
|
+
EOS
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
config.last
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def last_id
|
|
224
|
+
@last_id ||= episodes.find_index(last_safe)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def play
|
|
228
|
+
if name?
|
|
229
|
+
puts last_safe
|
|
230
|
+
else
|
|
231
|
+
$stderr.puts "Viewing #{last_safe}"
|
|
232
|
+
system(*viewer.split(' '), File.join(config.dir, last_safe))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
if update?
|
|
236
|
+
config.last_played_at = Time.now
|
|
237
|
+
config_save_safe(CFG_FILENAME, config)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def parse_config_value(param, value)
|
|
242
|
+
return if value.nil?
|
|
243
|
+
|
|
244
|
+
case param
|
|
245
|
+
when 'last'
|
|
246
|
+
parse_episode_ref(value)
|
|
247
|
+
when 'index_from_zero'
|
|
248
|
+
unless %w[true false].include? value
|
|
249
|
+
raise CommandError, <<~EOS
|
|
250
|
+
Invalid value '#{value}' for 'index_from_zero'.
|
|
251
|
+
Should be true or false.
|
|
252
|
+
EOS
|
|
253
|
+
end
|
|
254
|
+
value == "true"
|
|
255
|
+
when 'last_played_at'
|
|
256
|
+
begin
|
|
257
|
+
Time.parse value
|
|
258
|
+
rescue ArgumentError
|
|
259
|
+
raise CommandError, "Can't parse time"
|
|
260
|
+
end
|
|
261
|
+
when 'pointer'
|
|
262
|
+
"\"#{value}\"".undump
|
|
263
|
+
when 'formats'
|
|
264
|
+
value.split ','
|
|
265
|
+
else
|
|
266
|
+
value
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def config_save_safe(path, cfg)
|
|
271
|
+
File.open(path, 'w') { |io| cfg.save(io) }
|
|
272
|
+
rescue Errno::EACCES
|
|
273
|
+
raise CommandError, <<~EOS
|
|
274
|
+
Failed to save the episode data: can't write to this directory.
|
|
275
|
+
EOS
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'time'
|
|
3
|
+
|
|
4
|
+
class Episode
|
|
5
|
+
DEFAULT_CFG = {
|
|
6
|
+
viewer: 'mpv',
|
|
7
|
+
index_from_zero: false,
|
|
8
|
+
pointer: '*',
|
|
9
|
+
formats: %w[mkv mp4 avi]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class NotLocal < StandardError; end
|
|
13
|
+
|
|
14
|
+
class Config
|
|
15
|
+
attr_writer :viewer
|
|
16
|
+
attr_writer :index_from_zero
|
|
17
|
+
attr_writer :pointer
|
|
18
|
+
attr_writer :formats
|
|
19
|
+
|
|
20
|
+
def self.load(io, opts = {})
|
|
21
|
+
new JSON.parse(io.read, symbolize_names: true).merge(opts)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(opts = {})
|
|
25
|
+
@global_cfg = opts[:global]
|
|
26
|
+
|
|
27
|
+
unless global?
|
|
28
|
+
@local =
|
|
29
|
+
{
|
|
30
|
+
dir: opts[:dir],
|
|
31
|
+
last: opts[:last],
|
|
32
|
+
last_played_at: (Time.parse opts[:last_played_at] rescue nil)
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@viewer = opts[:viewer]
|
|
37
|
+
@index_from_zero = opts[:index_from_zero]
|
|
38
|
+
@pointer = opts[:pointer]
|
|
39
|
+
@formats = opts[:formats]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def save(io)
|
|
43
|
+
io.write JSON.pretty_generate(to_h(remove_defaults: true))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_h(remove_defaults: false)
|
|
47
|
+
if remove_defaults
|
|
48
|
+
(@local || {}).merge({
|
|
49
|
+
viewer: @viewer,
|
|
50
|
+
index_from_zero: @index_from_zero,
|
|
51
|
+
pointer: @pointer,
|
|
52
|
+
formats: @formats
|
|
53
|
+
}).compact
|
|
54
|
+
else
|
|
55
|
+
local_h = {
|
|
56
|
+
dir: dir,
|
|
57
|
+
last: last,
|
|
58
|
+
last_played_at: last_played_at
|
|
59
|
+
} unless global?
|
|
60
|
+
|
|
61
|
+
(local_h || {}).merge({
|
|
62
|
+
viewer: viewer,
|
|
63
|
+
index_from_zero: index_from_zero,
|
|
64
|
+
pointer: pointer,
|
|
65
|
+
formats: formats
|
|
66
|
+
})
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def global?
|
|
71
|
+
@global_cfg.nil?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def global
|
|
75
|
+
@global_cfg
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def dir
|
|
79
|
+
get_local(:dir) { Dir.pwd }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def dir=(new_val)
|
|
83
|
+
set_local :dir, new_val
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def last
|
|
87
|
+
get_local :last
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def last=(new_val)
|
|
91
|
+
set_local :last, new_val
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def last_played_at
|
|
95
|
+
get_local :last_played_at
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def last_played_at=(new_val)
|
|
99
|
+
set_local :last_played_at, new_val
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def viewer
|
|
103
|
+
@viewer || default(:viewer)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def index_from_zero
|
|
107
|
+
@index_from_zero || default(:index_from_zero)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def pointer
|
|
111
|
+
@pointer || default(:pointer)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def formats
|
|
115
|
+
@formats || default(:formats)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def get_local(param)
|
|
121
|
+
@local && @local[param] || (block_given? ? yield : default(param))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def set_local(param, new_val)
|
|
125
|
+
raise NotLocal if global?
|
|
126
|
+
@local[param] = new_val
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def default(param)
|
|
130
|
+
@global_cfg&.public_send(param) || DEFAULT_CFG[param]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: episode
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Maksim Esterkin
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2019-12-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Episode (ep) remembers what file in the directory you viewed last time.
|
|
14
|
+
Enumerates all files in the directory and allows to reference them only by a number.
|
|
15
|
+
email: esterkimx@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- ep
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE.md
|
|
22
|
+
- README.md
|
|
23
|
+
- bin/ep
|
|
24
|
+
- episode.gemspec
|
|
25
|
+
- lib/episode.rb
|
|
26
|
+
- lib/episode/config.rb
|
|
27
|
+
homepage: https://github.com/esterkimx/episode
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
bug_tracker_uri: https://github.com/esterkimx/episode/issues
|
|
32
|
+
documentation_uri: https://github.com/esterkimx/episode/blob/master/README.md
|
|
33
|
+
homepage_uri: https://github.com/esterkimx/episode
|
|
34
|
+
source_code_uri: https://github.com/esterkimx/episode
|
|
35
|
+
post_install_message:
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 2.3.0
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.0.6
|
|
51
|
+
signing_key:
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: Console assistant app for watching series
|
|
54
|
+
test_files: []
|