deadlist 1.0.1 → 1.1.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 +54 -0
- data/lib/deadlist/cli/client.rb +20 -43
- data/lib/deadlist/cli/downloader.rb +10 -4
- data/lib/deadlist/cli.rb +28 -19
- data/lib/deadlist/models/show.rb +33 -19
- data/lib/deadlist/models/track.rb +4 -19
- data/lib/deadlist.rb +10 -5
- metadata +7 -4
- 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: edb67e52d7cf77e3a3f693fa49c5939be32fb714b247b67a5e7dfd4dc64d4b34
|
4
|
+
data.tar.gz: f9bfc50fd08fd7586f5c542e0c540e8c5ffafee8480af8e9b8dfc1d1d6be3c02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76d8d658f14a546206b157a8765f48e1283f718e5c13864c0f7a605884f64e23235342f4767677b07a27e878cfc4cb00e96951fb5e85055efbcaafb69e3ea0dc
|
7
|
+
data.tar.gz: 48d2e2aead1b040a6034bfb946c7cb62048490c22796dfc9dfbcd4451822305758dd243c1ee3ad107c998697a16ce2155f29766f8aa5582354ba80948235d6f3
|
@@ -0,0 +1,54 @@
|
|
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 to download") do |id|
|
11
|
+
params[:id] = id
|
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("-h", "--help", "Show this help") do
|
22
|
+
puts opts
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("-v", "--version", "Show version") do
|
27
|
+
puts "deadlist v#{version}"
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
parser.parse!(args)
|
33
|
+
validate_required_params!(params, parser)
|
34
|
+
params
|
35
|
+
rescue OptionParser::InvalidOption => e
|
36
|
+
puts "Error: #{e.message}"
|
37
|
+
puts parser
|
38
|
+
exit(1)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def self.validate_required_params!(params, parser)
|
44
|
+
missing = []
|
45
|
+
missing << "--id" unless params[:id]
|
46
|
+
missing << "--format" unless params[:format]
|
47
|
+
|
48
|
+
unless missing.empty?
|
49
|
+
puts "Error: Missing required arguments: #{missing.join(', ')}"
|
50
|
+
puts parser
|
51
|
+
exit(1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
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
|
-
|
3
|
+
def query_show_info(show_id)
|
4
|
+
url = 'https://archive.org/metadata/' + show_id
|
5
|
+
response = HTTParty.get(url)
|
6
|
+
|
7
|
+
unless response.success?
|
8
|
+
raise "API request failed: #{response.code}"
|
9
|
+
end
|
10
|
+
|
11
|
+
unless response["metadata"]
|
12
|
+
raise "Invalid show ID: #{show_id}"
|
13
|
+
end
|
7
14
|
|
8
15
|
show_data = {
|
9
|
-
date:
|
10
|
-
location:
|
11
|
-
venue:
|
12
|
-
transferred_by:
|
13
|
-
duration:
|
14
|
-
|
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"]
|
15
23
|
}
|
16
24
|
|
17
25
|
return show_data
|
18
|
-
rescue => e
|
19
|
-
|
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
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# Hunts through track-divs for data required to create Tracks
|
43
|
-
def extract_track_data(track_divs)
|
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
|
-
}
|
50
|
-
end
|
26
|
+
rescue HTTParty::Error, StandardError => e
|
27
|
+
raise "Failed to fetch show data: #{e.message}"
|
51
28
|
end
|
52
29
|
end
|
@@ -1,17 +1,23 @@
|
|
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
|
+
BASE_API_URL = 'https://archive.org'
|
5
|
+
|
4
6
|
def initialize(path, format)
|
5
7
|
@path = path
|
6
8
|
@format = format
|
7
9
|
end
|
10
|
+
|
11
|
+
def download_url_for_show(show_id)
|
12
|
+
"#{BASE_API_URL}/download/#{show_id}/"
|
13
|
+
end
|
8
14
|
|
9
15
|
# 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
|
-
|
16
|
+
def get(root_url, track_object)
|
17
|
+
uri = URI.parse(root_url + track_object.filename); raise ArgumentError, "Only HTTP(S) URLs allowed" unless uri.is_a?(URI::HTTP)
|
13
18
|
download = uri.open
|
14
|
-
filename = "#{@path}/#{pos} -- #{
|
19
|
+
filename = "#{@path}/#{track_object.pos} -- #{track_object.title}.#{@format}"
|
20
|
+
|
15
21
|
IO.copy_stream(download, filename)
|
16
22
|
rescue => e
|
17
23
|
puts "❌ Download failed: #{e.message}"
|
data/lib/deadlist/cli.rb
CHANGED
@@ -2,7 +2,9 @@ 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.rb'
|
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
|
@@ -15,34 +17,26 @@ class CLI
|
|
15
17
|
parse_arguments(args)
|
16
18
|
end
|
17
19
|
|
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
20
|
# Creates new show object with link given populated with metadata and track details
|
27
|
-
def
|
28
|
-
|
29
|
-
|
21
|
+
def create_show
|
22
|
+
extracted_id = extract_show_id(@args[:id])
|
23
|
+
@show = Show.new(extracted_id, @args[:format])
|
24
|
+
|
25
|
+
puts "\n💿 #{@show.name} - #{@show.tracks.length} tracks found!"
|
30
26
|
rescue => e
|
31
27
|
puts "\n❌ Scraping failed: #{e.message}"
|
32
28
|
end
|
33
29
|
|
34
30
|
# Validates format isn't for test, and passes directory + format arguments to the download method of a Show
|
35
31
|
def download_show
|
36
|
-
|
37
|
-
|
38
|
-
if download_format == "test"
|
39
|
-
puts "Test Download, skipping"
|
40
|
-
elsif @show.has_format?(download_format)
|
41
|
-
download_path = setup_directories(@show)
|
42
|
-
@show.download_tracks(download_path, download_format)
|
32
|
+
if @args[:format] == "test"
|
33
|
+
puts "Test Download, skipping"
|
43
34
|
else
|
44
|
-
|
35
|
+
download_directory = setup_directories(@show)
|
36
|
+
@show.download_tracks(download_directory)
|
45
37
|
end
|
38
|
+
rescue => e
|
39
|
+
puts "\n❌ Download failed: #{e.message}"
|
46
40
|
end
|
47
41
|
|
48
42
|
private
|
@@ -55,6 +49,19 @@ class CLI
|
|
55
49
|
puts '='*52
|
56
50
|
end
|
57
51
|
|
52
|
+
# Reads arguments passed at the command line and maps them to an instance object
|
53
|
+
def extract_show_id(show_input)
|
54
|
+
if show_input.include?('archive.org/details/')
|
55
|
+
show_input.split('/details/').last
|
56
|
+
else
|
57
|
+
show_input
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_arguments(args)
|
62
|
+
@args = ArgumentParser.parse(args, @version)
|
63
|
+
end
|
64
|
+
|
58
65
|
# Configures directories that will be used by the downloader
|
59
66
|
def setup_directories(show, base_path = Dir.pwd)
|
60
67
|
# Create base shows directory
|
@@ -66,5 +73,7 @@ class CLI
|
|
66
73
|
FileUtils.mkdir_p(show_dir)
|
67
74
|
|
68
75
|
return show_dir
|
76
|
+
rescue => e
|
77
|
+
puts "\n❌ Directory creation failed: #{e.message}"
|
69
78
|
end
|
70
79
|
end
|
data/lib/deadlist/models/show.rb
CHANGED
@@ -2,35 +2,30 @@
|
|
2
2
|
class Show
|
3
3
|
attr_reader :name, :venue, :date, :location, :duration, :transferred_by, :tracks, :available_formats
|
4
4
|
|
5
|
-
def initialize(
|
6
|
-
@
|
5
|
+
def initialize(show_id, format)
|
6
|
+
@show_id = show_id
|
7
|
+
@format = format
|
7
8
|
@name = nil
|
8
9
|
@date = nil
|
9
10
|
@location = nil
|
10
11
|
@venue = nil
|
11
12
|
@duration = nil
|
12
13
|
@transferred_by = nil
|
13
|
-
@
|
14
|
+
@url = nil
|
14
15
|
@tracks = nil
|
15
16
|
|
16
17
|
set_show_info
|
17
18
|
end
|
18
19
|
|
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
20
|
# Initializes a Downloader and passes track details
|
25
|
-
def download_tracks(path
|
26
|
-
dl = Downloader.new(path, format)
|
21
|
+
def download_tracks(path)
|
22
|
+
dl = Downloader.new(path, @format)
|
23
|
+
download_url = dl.download_url_for_show(@show_id)
|
27
24
|
|
28
25
|
@tracks.each do |track|
|
29
|
-
|
30
|
-
|
31
|
-
dl.get(track.pos, track.name, track_link)
|
26
|
+
dl.get(download_url, track)
|
32
27
|
|
33
|
-
puts "⚡️ #{track.pos} - #{track.
|
28
|
+
puts "⚡️ #{track.pos} - #{track.title} downloaded successfully"
|
34
29
|
end
|
35
30
|
end
|
36
31
|
|
@@ -38,21 +33,40 @@ class Show
|
|
38
33
|
|
39
34
|
# On initialization, show variables are extracted from the HTML data scraped by the Client.
|
40
35
|
def set_show_info
|
41
|
-
show_data = Client.new.
|
42
|
-
|
36
|
+
show_data = Client.new.query_show_info(@show_id)
|
37
|
+
|
43
38
|
@date = show_data[:date]
|
44
39
|
@location = show_data[:location]
|
45
40
|
@venue = show_data[:venue]
|
46
41
|
@duration = show_data[:duration]
|
47
42
|
@transferred_by = show_data[:transferred_by]
|
48
43
|
@name = "#{show_data[:date]} - #{show_data[:venue]} - #{show_data[:location]}"
|
49
|
-
@tracks = set_tracks(show_data[:
|
44
|
+
@tracks = set_tracks(show_data[:files])
|
45
|
+
@url = "https://archive.org/metadata/" + show_data[:dir] + "/"
|
50
46
|
|
51
47
|
puts "🌹💀 Downloading #{name}"
|
52
48
|
end
|
53
49
|
|
54
50
|
# Converts track lists to Track objects
|
55
|
-
def set_tracks(
|
56
|
-
|
51
|
+
def set_tracks(files)
|
52
|
+
audio_files = files.select { |file| audio_file?(file) }
|
53
|
+
.select { |file| matches_format?(file, @format) }
|
54
|
+
|
55
|
+
if audio_files.empty?
|
56
|
+
puts "❌ No #{@format} files found"
|
57
|
+
return []
|
58
|
+
end
|
59
|
+
|
60
|
+
@tracks = audio_files.map { |track| Track.new(track) }
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def audio_file?(file)
|
66
|
+
%w[mp3 flac ogg m4a].include?(File.extname(file["name"]).delete('.'))
|
67
|
+
end
|
68
|
+
|
69
|
+
def matches_format?(file, format)
|
70
|
+
File.extname(file["name"]).delete('.') == format
|
57
71
|
end
|
58
72
|
end
|
@@ -1,24 +1,9 @@
|
|
1
1
|
class Track
|
2
|
-
attr_reader :pos, :
|
2
|
+
attr_reader :pos, :title, :filename
|
3
3
|
|
4
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)
|
5
|
+
@pos = track_data["track"]
|
6
|
+
@title = track_data["title"]
|
7
|
+
@filename = track_data["name"]
|
23
8
|
end
|
24
9
|
end
|
data/lib/deadlist.rb
CHANGED
@@ -7,20 +7,25 @@ require_relative 'deadlist/cli.rb'
|
|
7
7
|
|
8
8
|
# Main DeadList class.
|
9
9
|
class DeadList
|
10
|
-
HOSTNAME = 'https://www.archive.org/'
|
11
|
-
|
12
10
|
def initialize
|
13
|
-
@current_version = '1.0
|
14
|
-
@hostname = HOSTNAME
|
11
|
+
@current_version = '1.1.0'
|
15
12
|
end
|
16
13
|
|
14
|
+
# Argument abstraction should probably happen at this level!
|
15
|
+
|
17
16
|
def run
|
18
17
|
# Start a new CLI session
|
19
18
|
# In future this could be abstracted to pass the show link vs all args, so a 'session' is started per show.
|
20
19
|
session = CLI.new(@current_version, ARGV)
|
21
20
|
|
22
21
|
# Scrape links and metadata for given show
|
23
|
-
session.
|
22
|
+
session.create_show
|
23
|
+
|
24
|
+
# In future, consider starting multiple downloaders for a list of shows
|
25
|
+
# show_list = session.args[:shows]
|
26
|
+
# show_list.each do |show|
|
27
|
+
# session.download_show(show)
|
28
|
+
# end
|
24
29
|
|
25
30
|
# Create folder with show date and begin track downloads if format matches
|
26
31
|
session.download_show
|
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.1.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-08-18 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: httparty
|
@@ -48,15 +49,16 @@ files:
|
|
48
49
|
- bin/deadlist
|
49
50
|
- lib/deadlist.rb
|
50
51
|
- lib/deadlist/cli.rb
|
52
|
+
- lib/deadlist/cli/argument_parser.rb
|
51
53
|
- lib/deadlist/cli/client.rb
|
52
54
|
- lib/deadlist/cli/downloader.rb
|
53
55
|
- lib/deadlist/models/show.rb
|
54
56
|
- lib/deadlist/models/track.rb
|
55
|
-
- lib/version.rb
|
56
57
|
homepage: https://github.com/nazwr/deadlist
|
57
58
|
licenses:
|
58
59
|
- MIT
|
59
60
|
metadata: {}
|
61
|
+
post_install_message:
|
60
62
|
rdoc_options: []
|
61
63
|
require_paths:
|
62
64
|
- lib
|
@@ -71,7 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
73
|
- !ruby/object:Gem::Version
|
72
74
|
version: '0'
|
73
75
|
requirements: []
|
74
|
-
rubygems_version: 3.
|
76
|
+
rubygems_version: 3.4.19
|
77
|
+
signing_key:
|
75
78
|
specification_version: 4
|
76
79
|
summary: Download Grateful Dead shows from archive.org
|
77
80
|
test_files: []
|
data/lib/version.rb
DELETED