deadlist 1.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edb67e52d7cf77e3a3f693fa49c5939be32fb714b247b67a5e7dfd4dc64d4b34
4
- data.tar.gz: f9bfc50fd08fd7586f5c542e0c540e8c5ffafee8480af8e9b8dfc1d1d6be3c02
3
+ metadata.gz: a7a8fcc71296e7c196d233a170985d82a143da192420557a869ec6924bc165bc
4
+ data.tar.gz: 1bffff8c94561151b48ec9adabcdb418e7df13971b810f7deb61d5f4ceda678b
5
5
  SHA512:
6
- metadata.gz: 76d8d658f14a546206b157a8765f48e1283f718e5c13864c0f7a605884f64e23235342f4767677b07a27e878cfc4cb00e96951fb5e85055efbcaafb69e3ea0dc
7
- data.tar.gz: 48d2e2aead1b040a6034bfb946c7cb62048490c22796dfc9dfbcd4451822305758dd243c1ee3ad107c998697a16ce2155f29766f8aa5582354ba80948235d6f3
6
+ metadata.gz: 142ff3d85a10c2ead5b00a14692dbbd0736c3f9b0c8b7ff0ec5a1c92b278bd11038459ae2fc0f175b85035f7222fa8f7a705541315a60bda213beccc1e6247c8
7
+ data.tar.gz: 6c86fae1e34b504a2385e241ec97b54c1944db52caa4025d6d03f982c8d2199c1c07ec500277d0966a5b0c2d30a3383f844dc995d84087f7149dbd324b3f0d85
@@ -7,8 +7,8 @@ class ArgumentParser
7
7
  opts.separator ""
8
8
  opts.separator "Required options:"
9
9
 
10
- opts.on("-i", "--id ID", "ID of show to download") do |id|
11
- params[:id] = id
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
12
  end
13
13
 
14
14
  opts.on("-f", "--format FORMAT", "Format to download (mp3, flac, ogg)") do |format|
@@ -18,6 +18,10 @@ class ArgumentParser
18
18
  opts.separator ""
19
19
  opts.separator "Other options:"
20
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
+
21
25
  opts.on("-h", "--help", "Show this help") do
22
26
  puts opts
23
27
  exit
@@ -27,6 +31,14 @@ class ArgumentParser
27
31
  puts "deadlist v#{version}"
28
32
  exit
29
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
30
42
  end
31
43
 
32
44
  parser.parse!(args)
@@ -41,12 +53,20 @@ class ArgumentParser
41
53
  private
42
54
 
43
55
  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(', ')}"
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"
50
70
  puts parser
51
71
  exit(1)
52
72
  end
@@ -1,7 +1,7 @@
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
3
  def query_show_info(show_id)
4
- url = 'https://archive.org/metadata/' + show_id
4
+ url = "https://archive.org/metadata/#{show_id}"
5
5
  response = HTTParty.get(url)
6
6
 
7
7
  unless response.success?
@@ -22,7 +22,7 @@ class Client
22
22
  files: response["files"]
23
23
  }
24
24
 
25
- return show_data
25
+ show_data
26
26
  rescue HTTParty::Error, StandardError => e
27
27
  raise "Failed to fetch show data: #{e.message}"
28
28
  end
@@ -3,7 +3,8 @@
3
3
  class Downloader
4
4
  BASE_API_URL = 'https://archive.org'
5
5
 
6
- def initialize(path, format)
6
+ def initialize(path, format, logger: Logger.new($stdout))
7
+ @logger = logger
7
8
  @path = path
8
9
  @format = format
9
10
  end
@@ -16,10 +17,25 @@ class Downloader
16
17
  def get(root_url, track_object)
17
18
  uri = URI.parse(root_url + track_object.filename); raise ArgumentError, "Only HTTP(S) URLs allowed" unless uri.is_a?(URI::HTTP)
18
19
  download = uri.open
19
- filename = "#{@path}/#{track_object.pos} -- #{track_object.title}.#{@format}"
20
20
 
21
- IO.copy_stream(download, filename)
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
22
37
  rescue => e
23
- puts "❌ Download failed: #{e.message}"
38
+ @logger.error "❌ Download failed for '#{track_object.title}': #{e.message}"
39
+ false
24
40
  end
25
41
  end
data/lib/deadlist/cli.rb CHANGED
@@ -2,16 +2,22 @@ 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
+ require_relative 'cli/argument_parser'
6
6
  require 'fileutils'
7
7
  require 'optparse'
8
8
 
9
9
  # The CLI is the 'session' created by the main class, managing arguments passed in and housing methods for scraping and downloading shows.
10
10
  class CLI
11
- def initialize(version, args)
11
+ attr_reader :args, :show
12
+
13
+ def initialize(version, args, logger: Logger.new($stdout))
12
14
  @version = version
13
15
  @args = {}
14
16
  @show = nil
17
+ @logger = logger
18
+ @logger.formatter = proc do |severity, datetime, progname, msg|
19
+ "#{msg}\n"
20
+ end
15
21
 
16
22
  startup_text
17
23
  parse_arguments(args)
@@ -19,34 +25,37 @@ class CLI
19
25
 
20
26
  # Creates new show object with link given populated with metadata and track details
21
27
  def create_show
22
- extracted_id = extract_show_id(@args[:id])
23
- @show = Show.new(extracted_id, @args[:format])
28
+ show_id = @args[:ids].first
29
+ extracted_id = extract_show_id(show_id)
30
+ @show = Show.new(extracted_id, @args[:format], logger: @logger)
24
31
 
25
- puts "\n💿 #{@show.name} - #{@show.tracks.length} tracks found!"
32
+ @logger.info "💿 #{@show.name} - #{@show.tracks.length} tracks found!"
26
33
  rescue => e
27
- puts "\n❌ Scraping failed: #{e.message}"
34
+ @logger.error "❌ Scraping failed: #{e.message}"
28
35
  end
29
36
 
30
- # Validates format isn't for test, and passes directory + format arguments to the download method of a Show
37
+ # Downloads show tracks or displays dry-run preview
31
38
  def download_show
32
- if @args[:format] == "test"
33
- puts "Test Download, skipping"
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
34
44
  else
35
- download_directory = setup_directories(@show)
45
+ download_directory = setup_directories(@show, @args[:directory])
36
46
  @show.download_tracks(download_directory)
37
47
  end
38
48
  rescue => e
39
- puts "\n❌ Download failed: #{e.message}"
49
+ @logger.error "❌ Download failed: #{e.message}"
40
50
  end
41
51
 
42
52
  private
43
53
 
44
54
  # Deadlist starts with some friendly text
45
55
  def startup_text
46
- puts "\n\n"
47
- puts '='*52
48
- puts "🌹⚡️ One man gathers what another man spills... ⚡️🌹"
49
- puts '='*52
56
+ @logger.info '='*52
57
+ @logger.info "🌹⚡️ One man gathers what another man spills... ⚡️🌹"
58
+ @logger.info '='*52
50
59
  end
51
60
 
52
61
  # Reads arguments passed at the command line and maps them to an instance object
@@ -63,17 +72,22 @@ class CLI
63
72
  end
64
73
 
65
74
  # Configures directories that will be used by the downloader
66
- def setup_directories(show, base_path = Dir.pwd)
67
- # Create base shows directory
68
- shows_dir = File.join(base_path, "shows")
69
- FileUtils.mkdir_p(shows_dir)
70
-
71
- # Create specific show directory
72
- show_dir = File.join(shows_dir, show.name)
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)
73
87
  FileUtils.mkdir_p(show_dir)
74
88
 
75
- return show_dir
89
+ show_dir
76
90
  rescue => e
77
- puts "\n❌ Directory creation failed: #{e.message}"
91
+ @logger.error "❌ Directory creation failed: #{e.message}"
78
92
  end
79
- end
93
+ end
@@ -1,8 +1,11 @@
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(show_id, format)
7
+ def initialize(show_id, format, logger: Logger.new($stdout))
8
+ @logger = logger
6
9
  @show_id = show_id
7
10
  @format = format
8
11
  @name = nil
@@ -19,14 +22,21 @@ class Show
19
22
 
20
23
  # Initializes a Downloader and passes track details
21
24
  def download_tracks(path)
22
- dl = Downloader.new(path, @format)
25
+ dl = Downloader.new(path, @format, logger: @logger)
23
26
  download_url = dl.download_url_for_show(@show_id)
27
+ succesful_downloads = 0
24
28
 
25
29
  @tracks.each do |track|
26
- dl.get(download_url, track)
30
+ @logger.info "⬇️ Downloading #{track.pos} - #{track.title}..."
31
+
32
+ if dl.get(download_url, track)
33
+ succesful_downloads += 1
34
+ end
27
35
 
28
- puts "⚡️ #{track.pos} - #{track.title} downloaded successfully"
36
+ @logger.info "⚡️ #{track.pos} - #{track.title} downloaded successfully"
29
37
  end
38
+
39
+ @logger.info "✅ Downloaded #{succesful_downloads}/#{@tracks.length} tracks successfully!"
30
40
  end
31
41
 
32
42
  private
@@ -42,9 +52,9 @@ class Show
42
52
  @transferred_by = show_data[:transferred_by]
43
53
  @name = "#{show_data[:date]} - #{show_data[:venue]} - #{show_data[:location]}"
44
54
  @tracks = set_tracks(show_data[:files])
45
- @url = "https://archive.org/metadata/" + show_data[:dir] + "/"
55
+ @url = "https://archive.org/metadata/#{show_data[:dir]}/"
46
56
 
47
- puts "🌹💀 Downloading #{name}"
57
+ @logger.info "🌹💀 Show #{name} found!"
48
58
  end
49
59
 
50
60
  # Converts track lists to Track objects
@@ -53,20 +63,18 @@ class Show
53
63
  .select { |file| matches_format?(file, @format) }
54
64
 
55
65
  if audio_files.empty?
56
- puts "❌ No #{@format} files found"
66
+ @logger.error "❌ No #{@format} files found"
57
67
  return []
58
68
  end
59
69
 
60
- @tracks = audio_files.map { |track| Track.new(track) }
70
+ @tracks = audio_files.map.with_index(1) { |track, index| Track.new(track, index) }
61
71
  end
62
72
 
63
- private
64
-
65
73
  def audio_file?(file)
66
- %w[mp3 flac ogg m4a].include?(File.extname(file["name"]).delete('.'))
74
+ AUDIO_FORMATS.include?(File.extname(file["name"]).delete('.').downcase)
67
75
  end
68
76
 
69
77
  def matches_format?(file, format)
70
- File.extname(file["name"]).delete('.') == format
78
+ File.extname(file["name"]).delete('.').downcase == format
71
79
  end
72
80
  end
@@ -1,8 +1,8 @@
1
1
  class Track
2
2
  attr_reader :pos, :title, :filename
3
3
 
4
- def initialize(track_data)
5
- @pos = track_data["track"]
4
+ def initialize(track_data, index)
5
+ @pos = track_data["track"] || index
6
6
  @title = track_data["title"]
7
7
  @filename = track_data["name"]
8
8
  end
@@ -0,0 +1,3 @@
1
+ class DeadList
2
+ VERSION = '1.2.0'
3
+ end
data/lib/deadlist.rb CHANGED
@@ -1,34 +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/cli.rb'
6
+ require_relative 'deadlist/version'
7
+ require_relative 'deadlist/cli'
7
8
 
8
9
  # Main DeadList class.
9
10
  class DeadList
10
- def initialize
11
- @current_version = '1.1.0'
11
+ attr_reader :current_version
12
+
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
12
20
  end
13
21
 
14
- # Argument abstraction should probably happen at this level!
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]
15
41
 
16
- def run
17
- # Start a new CLI session
18
- # In future this could be abstracted to pass the show link vs all args, so a 'session' is started per show.
19
- session = CLI.new(@current_version, ARGV)
42
+ # Create CLI session for this show
43
+ session = CLI.new(@current_version, show_argv, logger: @logger)
20
44
 
21
45
  # Scrape links and metadata for given show
22
46
  session.create_show
23
47
 
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
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
29
57
 
30
- # Create folder with show date and begin track downloads if format matches
58
+ # Create folder and begin track downloads
31
59
  session.download_show
60
+ end
32
61
  end
33
62
  end
34
63
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deadlist
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nazwr
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-18 00:00:00.000000000 Z
11
+ date: 2025-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.21'
27
- - !ruby/object:Gem::Dependency
28
- name: nokogiri
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.10'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.10'
41
27
  description: A Ruby gem for downloading Grateful Dead concert recordings from the
42
28
  Internet Archive
43
29
  email: nathan@azotiwright.com
@@ -54,6 +40,7 @@ files:
54
40
  - lib/deadlist/cli/downloader.rb
55
41
  - lib/deadlist/models/show.rb
56
42
  - lib/deadlist/models/track.rb
43
+ - lib/deadlist/version.rb
57
44
  homepage: https://github.com/nazwr/deadlist
58
45
  licenses:
59
46
  - MIT