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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cc3ef0661dbc9d67bc88bae8f63b3b42a273e3376647cfee377b9466c23c0d1
4
- data.tar.gz: 5f2424f582c3c5c27f102cf842d64083c35b1d20e675e5513b1767014b064673
3
+ metadata.gz: edb67e52d7cf77e3a3f693fa49c5939be32fb714b247b67a5e7dfd4dc64d4b34
4
+ data.tar.gz: f9bfc50fd08fd7586f5c542e0c540e8c5ffafee8480af8e9b8dfc1d1d6be3c02
5
5
  SHA512:
6
- metadata.gz: 5e1ca2667a3a681a0119da589919ed8486b416d7963eeb63a318605a4a0d0d1ad85d02b5c8b086587cbe92101992aedaf9fa6bb39a953ae864ddad32f154ad5a
7
- data.tar.gz: 1f64dfee13234319fcbc450192d510e900add592839b32956950fa40c9c8e0cff1383edaec206fc5febaa88d4c1a265011cc089548f7e157265273a8e27097a2
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
@@ -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
- # Returns a show_data object for helping in the creation of a new Show
4
- def scrape_show_info(show_link)
5
- doc = get_page_source(show_link)
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)
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: extract_metadata(doc, itemprop: 'datePublished'),
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)
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
- 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
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(pos, name, link)
11
- uri = URI.parse(link); raise ArgumentError, "Only HTTP(S) URLs allowed" unless uri.is_a?(URI::HTTP)
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} -- #{name}.#{@format}"
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 scrape_links
28
- @show = Show.new(@args[:show])
29
- puts "\n💿 #{@show.tracks.length} tracks found!"
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
- download_format = @args[:format]
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
- puts "\n❌ #{download_format} not found for this show! #{@show.tracks[0].available_formats} available"
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
@@ -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(download_url)
6
- @show_link = download_url
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
- @available_formats = []
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, format)
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
- track_link = track.url_for_format(format)
30
-
31
- dl.get(track.pos, track.name, track_link)
26
+ dl.get(download_url, track)
32
27
 
33
- puts "⚡️ #{track.pos} - #{track.name} downloaded successfully"
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.scrape_show_info(@show_link)
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[:tracks])
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(track_data)
56
- @tracks = track_data.map { |track| Track.new(track) }
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, :name, :links
2
+ attr_reader :pos, :title, :filename
3
3
 
4
4
  def initialize(track_data)
5
- @pos = track_data[:pos]
6
- @name = track_data[:name]
7
- @links = track_data[:links]
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.1'
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.scrape_links
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.1
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: 1980-01-02 00:00:00.000000000 Z
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.6.7
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
@@ -1,3 +0,0 @@
1
- module DeadList
2
- VERSION = '1.0.0'
3
- end