deadlist 1.0.1 → 1.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 +4 -4
- data/lib/deadlist/cli/argument_parser.rb +74 -0
- data/lib/deadlist/cli/client.rb +21 -44
- data/lib/deadlist/cli/downloader.rb +29 -7
- data/lib/deadlist/cli.rb +58 -35
- data/lib/deadlist/models/show.rb +41 -19
- data/lib/deadlist/models/track.rb +5 -20
- data/lib/deadlist/version.rb +3 -0
- data/lib/deadlist.rb +46 -12
- metadata +8 -18
- data/lib/version.rb +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7a8fcc71296e7c196d233a170985d82a143da192420557a869ec6924bc165bc
|
|
4
|
+
data.tar.gz: 1bffff8c94561151b48ec9adabcdb418e7df13971b810f7deb61d5f4ceda678b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 142ff3d85a10c2ead5b00a14692dbbd0736c3f9b0c8b7ff0ec5a1c92b278bd11038459ae2fc0f175b85035f7222fa8f7a705541315a60bda213beccc1e6247c8
|
|
7
|
+
data.tar.gz: 6c86fae1e34b504a2385e241ec97b54c1944db52caa4025d6d03f982c8d2199c1c07ec500277d0966a5b0c2d30a3383f844dc995d84087f7149dbd324b3f0d85
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
class ArgumentParser
|
|
2
|
+
def self.parse(args, version)
|
|
3
|
+
params = {}
|
|
4
|
+
|
|
5
|
+
parser = OptionParser.new do |opts|
|
|
6
|
+
opts.banner = "Usage: deadlist [options]"
|
|
7
|
+
opts.separator ""
|
|
8
|
+
opts.separator "Required options:"
|
|
9
|
+
|
|
10
|
+
opts.on("-i", "--id ID", "ID of show(s) to download (comma-separated for multiple)") do |id|
|
|
11
|
+
params[:ids] = id.split(',').map(&:strip)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
opts.on("-f", "--format FORMAT", "Format to download (mp3, flac, ogg)") do |format|
|
|
15
|
+
params[:format] = format.downcase
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
opts.separator ""
|
|
19
|
+
opts.separator "Other options:"
|
|
20
|
+
|
|
21
|
+
opts.on("-d", "--directory PATH", "Directory to save show(s) to. Will default to /shows/ in the execution directory") do |dir|
|
|
22
|
+
params[:directory] = dir
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on("-h", "--help", "Show this help") do
|
|
26
|
+
puts opts
|
|
27
|
+
exit
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
opts.on("-v", "--version", "Show version") do
|
|
31
|
+
puts "deadlist v#{version}"
|
|
32
|
+
exit
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
opts.on('-q', '--quiet', 'Run silently') do
|
|
36
|
+
params[:quiet] = true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
opts.on('--dry-run', 'List tracks to be downloaded') do
|
|
40
|
+
params[:dry_run] = true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
parser.parse!(args)
|
|
45
|
+
validate_required_params!(params, parser)
|
|
46
|
+
params
|
|
47
|
+
rescue OptionParser::InvalidOption => e
|
|
48
|
+
puts "Error: #{e.message}"
|
|
49
|
+
puts parser
|
|
50
|
+
exit(1)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def self.validate_required_params!(params, parser)
|
|
56
|
+
has_ids = params[:ids]&.any?
|
|
57
|
+
has_format = params[:format]
|
|
58
|
+
|
|
59
|
+
# If one is provided, both must be provided
|
|
60
|
+
if !has_ids && !has_format
|
|
61
|
+
puts "Error: Arguments are required for DeadList, try --help for more info"
|
|
62
|
+
puts parser
|
|
63
|
+
exit(1)
|
|
64
|
+
elsif has_ids && !has_format
|
|
65
|
+
puts "Error: --format is required when --id is provided"
|
|
66
|
+
puts parser
|
|
67
|
+
exit(1)
|
|
68
|
+
elsif has_format && !has_ids
|
|
69
|
+
puts "Error: --id is required when --format is provided"
|
|
70
|
+
puts parser
|
|
71
|
+
exit(1)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/deadlist/cli/client.rb
CHANGED
|
@@ -1,52 +1,29 @@
|
|
|
1
1
|
# The Client class manages HTML scraping and parsing for the CLI and other classes above it. Any HTML work should be handled here.
|
|
2
2
|
class Client
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
track_divs = doc.css('div[itemprop="track"]')
|
|
3
|
+
def query_show_info(show_id)
|
|
4
|
+
url = "https://archive.org/metadata/#{show_id}"
|
|
5
|
+
response = HTTParty.get(url)
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
location: extract_metadata(doc, label: 'Location'),
|
|
11
|
-
venue: extract_metadata(doc, label: 'Venue'),
|
|
12
|
-
transferred_by: extract_metadata(doc, label: 'Transferred by'),
|
|
13
|
-
duration: extract_metadata(doc, label: 'Run time'),
|
|
14
|
-
tracks: extract_track_data(track_divs)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return show_data
|
|
18
|
-
rescue => e
|
|
19
|
-
puts "\n❌ Data extraction failed: #{e.message}"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
# Returns nokogiri-fied page HTML for use in scraping show info
|
|
25
|
-
def get_page_source(show_link)
|
|
26
|
-
return Nokogiri::HTML(HTTParty.get(show_link).body)
|
|
27
|
-
rescue => e
|
|
28
|
-
puts "\n❌ Scraping failed: #{e.message}"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Handles finding of values via 'label' and 'itemprop' Xpath values
|
|
32
|
-
def extract_metadata(doc, label: nil, itemprop: nil)
|
|
33
|
-
if label
|
|
34
|
-
# For dt/dd metadata pairs
|
|
35
|
-
doc.xpath("//dt[normalize-space(text())='#{label}']/following-sibling::dd").first&.text&.strip
|
|
36
|
-
elsif itemprop
|
|
37
|
-
# For itemprop attributes
|
|
38
|
-
doc.xpath("//*[@itemprop='#{itemprop}']").first&.content&.strip
|
|
7
|
+
unless response.success?
|
|
8
|
+
raise "API request failed: #{response.code}"
|
|
39
9
|
end
|
|
40
|
-
end
|
|
41
10
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
track_divs.each_with_index.map do |div, i|
|
|
45
|
-
{
|
|
46
|
-
pos: i + 1,
|
|
47
|
-
name: div.css('meta[itemprop="name"]').first&.[]('content'),
|
|
48
|
-
links: div.css('link[itemprop="associatedMedia"]').map { |link| link['href'] }
|
|
49
|
-
}
|
|
11
|
+
unless response["metadata"]
|
|
12
|
+
raise "Invalid show ID: #{show_id}"
|
|
50
13
|
end
|
|
14
|
+
|
|
15
|
+
show_data = {
|
|
16
|
+
date: response["metadata"]["date"],
|
|
17
|
+
location: response["metadata"]["coverage"],
|
|
18
|
+
venue: response["metadata"]["venue"],
|
|
19
|
+
transferred_by: response["metadata"]["transferer"],
|
|
20
|
+
duration: response["metadata"]["runtime"],
|
|
21
|
+
dir: response["metadata"]["identifier"],
|
|
22
|
+
files: response["files"]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
show_data
|
|
26
|
+
rescue HTTParty::Error, StandardError => e
|
|
27
|
+
raise "Failed to fetch show data: #{e.message}"
|
|
51
28
|
end
|
|
52
29
|
end
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
# A simple class to download files to a given directory. Expects details for the filename and a link.
|
|
2
2
|
# One Downloader should be created / show being downloaded. Downloaders can run on seperate threads for getting many hows at once.
|
|
3
3
|
class Downloader
|
|
4
|
-
|
|
4
|
+
BASE_API_URL = 'https://archive.org'
|
|
5
|
+
|
|
6
|
+
def initialize(path, format, logger: Logger.new($stdout))
|
|
7
|
+
@logger = logger
|
|
5
8
|
@path = path
|
|
6
9
|
@format = format
|
|
7
10
|
end
|
|
11
|
+
|
|
12
|
+
def download_url_for_show(show_id)
|
|
13
|
+
"#{BASE_API_URL}/download/#{show_id}/"
|
|
14
|
+
end
|
|
8
15
|
|
|
9
16
|
# Goes to a link (assuming the format is already validated), and gets the file, saving with argument names.
|
|
10
|
-
def get(
|
|
11
|
-
uri = URI.parse(
|
|
12
|
-
|
|
17
|
+
def get(root_url, track_object)
|
|
18
|
+
uri = URI.parse(root_url + track_object.filename); raise ArgumentError, "Only HTTP(S) URLs allowed" unless uri.is_a?(URI::HTTP)
|
|
13
19
|
download = uri.open
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
|
|
21
|
+
# Extract disc number from filename
|
|
22
|
+
disc_match = track_object.filename.match(/(?<!g)d(\d+)/)
|
|
23
|
+
sanitized_title = track_object.title.gsub('/', '-')
|
|
24
|
+
|
|
25
|
+
if disc_match
|
|
26
|
+
# Multi-disc: use disc-track format (1-01, 2-01, etc.)
|
|
27
|
+
disc_num = disc_match[1]
|
|
28
|
+
padded_track = track_object.pos.rjust(2, '0')
|
|
29
|
+
filename = "#{@path}/#{disc_num}-#{padded_track} -- #{sanitized_title}.#{@format}"
|
|
30
|
+
else
|
|
31
|
+
# Single disc: regular format
|
|
32
|
+
filename = "#{@path}/#{track_object.pos} -- #{sanitized_title}.#{@format}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
IO.copy_stream(download, filename)
|
|
36
|
+
true
|
|
16
37
|
rescue => e
|
|
17
|
-
|
|
38
|
+
@logger.error "❌ Download failed for '#{track_object.title}': #{e.message}"
|
|
39
|
+
false
|
|
18
40
|
end
|
|
19
41
|
end
|
data/lib/deadlist/cli.rb
CHANGED
|
@@ -2,69 +2,92 @@ require_relative 'cli/client'
|
|
|
2
2
|
require_relative 'cli/downloader'
|
|
3
3
|
require_relative 'models/show'
|
|
4
4
|
require_relative 'models/track'
|
|
5
|
+
require_relative 'cli/argument_parser'
|
|
5
6
|
require 'fileutils'
|
|
7
|
+
require 'optparse'
|
|
6
8
|
|
|
7
9
|
# The CLI is the 'session' created by the main class, managing arguments passed in and housing methods for scraping and downloading shows.
|
|
8
10
|
class CLI
|
|
9
|
-
|
|
11
|
+
attr_reader :args, :show
|
|
12
|
+
|
|
13
|
+
def initialize(version, args, logger: Logger.new($stdout))
|
|
10
14
|
@version = version
|
|
11
15
|
@args = {}
|
|
12
16
|
@show = nil
|
|
17
|
+
@logger = logger
|
|
18
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
|
19
|
+
"#{msg}\n"
|
|
20
|
+
end
|
|
13
21
|
|
|
14
22
|
startup_text
|
|
15
23
|
parse_arguments(args)
|
|
16
24
|
end
|
|
17
25
|
|
|
18
|
-
# Reads arguments passed at the command line and maps them to an instance object
|
|
19
|
-
def parse_arguments(args)
|
|
20
|
-
args.each do |arg|
|
|
21
|
-
key, value = arg.split('=')
|
|
22
|
-
@args[key.tr('--', '').to_sym] = value
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
26
|
# Creates new show object with link given populated with metadata and track details
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
def create_show
|
|
28
|
+
show_id = @args[:ids].first
|
|
29
|
+
extracted_id = extract_show_id(show_id)
|
|
30
|
+
@show = Show.new(extracted_id, @args[:format], logger: @logger)
|
|
31
|
+
|
|
32
|
+
@logger.info "💿 #{@show.name} - #{@show.tracks.length} tracks found!"
|
|
30
33
|
rescue => e
|
|
31
|
-
|
|
34
|
+
@logger.error "❌ Scraping failed: #{e.message}"
|
|
32
35
|
end
|
|
33
36
|
|
|
34
|
-
#
|
|
37
|
+
# Downloads show tracks or displays dry-run preview
|
|
35
38
|
def download_show
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
download_path = setup_directories(@show)
|
|
42
|
-
@show.download_tracks(download_path, download_format)
|
|
39
|
+
if @args[:dry_run]
|
|
40
|
+
@logger.info "🔍 Dry Run: #{@show.name} will be downloaded with #{@show.tracks.count} tracks"
|
|
41
|
+
@show.tracks.each do |track|
|
|
42
|
+
@logger.info " #{track.pos} - #{track.title}"
|
|
43
|
+
end
|
|
43
44
|
else
|
|
44
|
-
|
|
45
|
+
download_directory = setup_directories(@show, @args[:directory])
|
|
46
|
+
@show.download_tracks(download_directory)
|
|
45
47
|
end
|
|
48
|
+
rescue => e
|
|
49
|
+
@logger.error "❌ Download failed: #{e.message}"
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
private
|
|
49
53
|
|
|
50
54
|
# Deadlist starts with some friendly text
|
|
51
55
|
def startup_text
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
@logger.info '='*52
|
|
57
|
+
@logger.info "🌹⚡️ One man gathers what another man spills... ⚡️🌹"
|
|
58
|
+
@logger.info '='*52
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reads arguments passed at the command line and maps them to an instance object
|
|
62
|
+
def extract_show_id(show_input)
|
|
63
|
+
if show_input.include?('archive.org/details/')
|
|
64
|
+
show_input.split('/details/').last
|
|
65
|
+
else
|
|
66
|
+
show_input
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_arguments(args)
|
|
71
|
+
@args = ArgumentParser.parse(args, @version)
|
|
56
72
|
end
|
|
57
73
|
|
|
58
74
|
# Configures directories that will be used by the downloader
|
|
59
|
-
def setup_directories(show,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
def setup_directories(show, custom_path = nil)
|
|
76
|
+
if custom_path
|
|
77
|
+
# Custom path: use it directly
|
|
78
|
+
base_dir = custom_path
|
|
79
|
+
else
|
|
80
|
+
# Default: add shows subdirectory
|
|
81
|
+
base_dir = File.join(Dir.pwd, "shows")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
FileUtils.mkdir_p(base_dir)
|
|
85
|
+
|
|
86
|
+
show_dir = File.join(base_dir, show.name)
|
|
66
87
|
FileUtils.mkdir_p(show_dir)
|
|
67
88
|
|
|
68
|
-
|
|
89
|
+
show_dir
|
|
90
|
+
rescue => e
|
|
91
|
+
@logger.error "❌ Directory creation failed: #{e.message}"
|
|
69
92
|
end
|
|
70
|
-
end
|
|
93
|
+
end
|
data/lib/deadlist/models/show.rb
CHANGED
|
@@ -1,58 +1,80 @@
|
|
|
1
1
|
# Object to handle Show data and the array of Track objects to be used in downloading.
|
|
2
2
|
class Show
|
|
3
|
+
AUDIO_FORMATS = %w[mp3 flac ogg m4a].freeze
|
|
4
|
+
|
|
3
5
|
attr_reader :name, :venue, :date, :location, :duration, :transferred_by, :tracks, :available_formats
|
|
4
6
|
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
7
|
+
def initialize(show_id, format, logger: Logger.new($stdout))
|
|
8
|
+
@logger = logger
|
|
9
|
+
@show_id = show_id
|
|
10
|
+
@format = format
|
|
7
11
|
@name = nil
|
|
8
12
|
@date = nil
|
|
9
13
|
@location = nil
|
|
10
14
|
@venue = nil
|
|
11
15
|
@duration = nil
|
|
12
16
|
@transferred_by = nil
|
|
13
|
-
@
|
|
17
|
+
@url = nil
|
|
14
18
|
@tracks = nil
|
|
15
19
|
|
|
16
20
|
set_show_info
|
|
17
21
|
end
|
|
18
22
|
|
|
19
|
-
# Returns whether or not a given format is available for this show
|
|
20
|
-
def has_format?(requested_format)
|
|
21
|
-
@tracks[0].has_format?(requested_format)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
23
|
# Initializes a Downloader and passes track details
|
|
25
|
-
def download_tracks(path
|
|
26
|
-
dl = Downloader.new(path, format)
|
|
24
|
+
def download_tracks(path)
|
|
25
|
+
dl = Downloader.new(path, @format, logger: @logger)
|
|
26
|
+
download_url = dl.download_url_for_show(@show_id)
|
|
27
|
+
succesful_downloads = 0
|
|
27
28
|
|
|
28
29
|
@tracks.each do |track|
|
|
29
|
-
|
|
30
|
+
@logger.info "⬇️ Downloading #{track.pos} - #{track.title}..."
|
|
30
31
|
|
|
31
|
-
dl.get(
|
|
32
|
+
if dl.get(download_url, track)
|
|
33
|
+
succesful_downloads += 1
|
|
34
|
+
end
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
@logger.info "⚡️ #{track.pos} - #{track.title} downloaded successfully"
|
|
34
37
|
end
|
|
38
|
+
|
|
39
|
+
@logger.info "✅ Downloaded #{succesful_downloads}/#{@tracks.length} tracks successfully!"
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
private
|
|
38
43
|
|
|
39
44
|
# On initialization, show variables are extracted from the HTML data scraped by the Client.
|
|
40
45
|
def set_show_info
|
|
41
|
-
show_data = Client.new.
|
|
42
|
-
|
|
46
|
+
show_data = Client.new.query_show_info(@show_id)
|
|
47
|
+
|
|
43
48
|
@date = show_data[:date]
|
|
44
49
|
@location = show_data[:location]
|
|
45
50
|
@venue = show_data[:venue]
|
|
46
51
|
@duration = show_data[:duration]
|
|
47
52
|
@transferred_by = show_data[:transferred_by]
|
|
48
53
|
@name = "#{show_data[:date]} - #{show_data[:venue]} - #{show_data[:location]}"
|
|
49
|
-
@tracks = set_tracks(show_data[:
|
|
54
|
+
@tracks = set_tracks(show_data[:files])
|
|
55
|
+
@url = "https://archive.org/metadata/#{show_data[:dir]}/"
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
@logger.info "🌹💀 Show #{name} found!"
|
|
52
58
|
end
|
|
53
59
|
|
|
54
60
|
# Converts track lists to Track objects
|
|
55
|
-
def set_tracks(
|
|
56
|
-
|
|
61
|
+
def set_tracks(files)
|
|
62
|
+
audio_files = files.select { |file| audio_file?(file) }
|
|
63
|
+
.select { |file| matches_format?(file, @format) }
|
|
64
|
+
|
|
65
|
+
if audio_files.empty?
|
|
66
|
+
@logger.error "❌ No #{@format} files found"
|
|
67
|
+
return []
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@tracks = audio_files.map.with_index(1) { |track, index| Track.new(track, index) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def audio_file?(file)
|
|
74
|
+
AUDIO_FORMATS.include?(File.extname(file["name"]).delete('.').downcase)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def matches_format?(file, format)
|
|
78
|
+
File.extname(file["name"]).delete('.').downcase == format
|
|
57
79
|
end
|
|
58
80
|
end
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
class Track
|
|
2
|
-
attr_reader :pos, :
|
|
2
|
+
attr_reader :pos, :title, :filename
|
|
3
3
|
|
|
4
|
-
def initialize(track_data)
|
|
5
|
-
@pos = track_data[
|
|
6
|
-
@
|
|
7
|
-
@
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Returns formats available for a given track via the links
|
|
11
|
-
def available_formats
|
|
12
|
-
@available_formats ||= links.map { |url| File.extname(url).delete('.') }
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Based on the format argument, returns one link containing that format
|
|
16
|
-
def url_for_format(format)
|
|
17
|
-
links.find { |url| url.end_with?(".#{format}") }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Returns boolean if a format exists for this Track
|
|
21
|
-
def has_format?(format)
|
|
22
|
-
available_formats.include?(format)
|
|
4
|
+
def initialize(track_data, index)
|
|
5
|
+
@pos = track_data["track"] || index
|
|
6
|
+
@title = track_data["title"]
|
|
7
|
+
@filename = track_data["name"]
|
|
23
8
|
end
|
|
24
9
|
end
|
data/lib/deadlist.rb
CHANGED
|
@@ -1,29 +1,63 @@
|
|
|
1
1
|
require 'httparty'
|
|
2
|
-
require 'nokogiri'
|
|
3
2
|
require 'open-uri'
|
|
4
3
|
require 'pry'
|
|
4
|
+
require 'logger'
|
|
5
5
|
|
|
6
|
-
require_relative 'deadlist/
|
|
6
|
+
require_relative 'deadlist/version'
|
|
7
|
+
require_relative 'deadlist/cli'
|
|
7
8
|
|
|
8
9
|
# Main DeadList class.
|
|
9
10
|
class DeadList
|
|
10
|
-
|
|
11
|
+
attr_reader :current_version
|
|
11
12
|
|
|
12
|
-
def initialize
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
def initialize(logger: Logger.new($stdout))
|
|
14
|
+
@logger = logger
|
|
15
|
+
@logger.level = Logger::INFO # Default level
|
|
16
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
|
17
|
+
"#{msg}\n"
|
|
18
|
+
end
|
|
19
|
+
@current_version = VERSION
|
|
15
20
|
end
|
|
16
21
|
|
|
17
|
-
def run
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
def run(argv = ARGV)
|
|
23
|
+
# Parse arguments to get show IDs and options
|
|
24
|
+
parsed_args = ArgumentParser.parse(argv, @current_version)
|
|
25
|
+
show_ids = parsed_args[:ids]
|
|
26
|
+
|
|
27
|
+
# Check for --quiet flag and adjust logger level
|
|
28
|
+
if parsed_args[:quiet]
|
|
29
|
+
@logger.level = Logger::ERROR
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Process each show
|
|
33
|
+
show_ids.each_with_index do |show_id, index|
|
|
34
|
+
@logger.info "📻 Processing show #{index + 1}/#{show_ids.count}: #{show_id}"
|
|
35
|
+
|
|
36
|
+
# Build arguments for this specific show
|
|
37
|
+
show_argv = ['--id', show_id, '--format', parsed_args[:format]]
|
|
38
|
+
show_argv += ['--directory', parsed_args[:directory]] if parsed_args[:directory]
|
|
39
|
+
show_argv += ['--quiet'] if parsed_args[:quiet]
|
|
40
|
+
show_argv += ['--dry-run'] if parsed_args[:dry_run]
|
|
41
|
+
|
|
42
|
+
# Create CLI session for this show
|
|
43
|
+
session = CLI.new(@current_version, show_argv, logger: @logger)
|
|
21
44
|
|
|
22
45
|
# Scrape links and metadata for given show
|
|
23
|
-
session.
|
|
46
|
+
session.create_show
|
|
47
|
+
|
|
48
|
+
# Check if show has tracks in requested format
|
|
49
|
+
if session.show && session.show.tracks.empty?
|
|
50
|
+
@logger.error "❌ #{show_id} not available in #{parsed_args[:format]} format"
|
|
51
|
+
if session.show.available_formats && !session.show.available_formats.empty?
|
|
52
|
+
@logger.error " Available formats: #{session.show.available_formats.join(', ')}"
|
|
53
|
+
end
|
|
54
|
+
@logger.error " Skipping..."
|
|
55
|
+
next # Skip to next show
|
|
56
|
+
end
|
|
24
57
|
|
|
25
|
-
# Create folder
|
|
58
|
+
# Create folder and begin track downloads
|
|
26
59
|
session.download_show
|
|
60
|
+
end
|
|
27
61
|
end
|
|
28
62
|
end
|
|
29
63
|
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deadlist
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nazwr
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2025-12-14 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: httparty
|
|
@@ -23,20 +24,6 @@ dependencies:
|
|
|
23
24
|
- - "~>"
|
|
24
25
|
- !ruby/object:Gem::Version
|
|
25
26
|
version: '0.21'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: nokogiri
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.10'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.10'
|
|
40
27
|
description: A Ruby gem for downloading Grateful Dead concert recordings from the
|
|
41
28
|
Internet Archive
|
|
42
29
|
email: nathan@azotiwright.com
|
|
@@ -48,15 +35,17 @@ files:
|
|
|
48
35
|
- bin/deadlist
|
|
49
36
|
- lib/deadlist.rb
|
|
50
37
|
- lib/deadlist/cli.rb
|
|
38
|
+
- lib/deadlist/cli/argument_parser.rb
|
|
51
39
|
- lib/deadlist/cli/client.rb
|
|
52
40
|
- lib/deadlist/cli/downloader.rb
|
|
53
41
|
- lib/deadlist/models/show.rb
|
|
54
42
|
- lib/deadlist/models/track.rb
|
|
55
|
-
- lib/version.rb
|
|
43
|
+
- lib/deadlist/version.rb
|
|
56
44
|
homepage: https://github.com/nazwr/deadlist
|
|
57
45
|
licenses:
|
|
58
46
|
- MIT
|
|
59
47
|
metadata: {}
|
|
48
|
+
post_install_message:
|
|
60
49
|
rdoc_options: []
|
|
61
50
|
require_paths:
|
|
62
51
|
- lib
|
|
@@ -71,7 +60,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
71
60
|
- !ruby/object:Gem::Version
|
|
72
61
|
version: '0'
|
|
73
62
|
requirements: []
|
|
74
|
-
rubygems_version: 3.
|
|
63
|
+
rubygems_version: 3.4.19
|
|
64
|
+
signing_key:
|
|
75
65
|
specification_version: 4
|
|
76
66
|
summary: Download Grateful Dead shows from archive.org
|
|
77
67
|
test_files: []
|
data/lib/version.rb
DELETED