arxiv-dl 0.2.0 → 0.3.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: 36af277147beae3a55221ba50443204db5b9e4bdae1613a3d9e923a465d4bc31
4
- data.tar.gz: 04d405743856249d2f78d4f9e711e73b716ddb27f1b6633ad92a96e6385677be
3
+ metadata.gz: c919d2e16ae72397375aa3aa5f71bd08fe4f3d0687d83358bf652081ee994596
4
+ data.tar.gz: 35a55cb7152642e19816a76b4a00fe52b06f74d5a52cb587d2e2b6897e23ac61
5
5
  SHA512:
6
- metadata.gz: 92527994ad88aec89a64f3f678f60ff8a9d286d3189d06a6eafa4a9ba4f5a02fa815e9d23c0e429c0ea44ad03d3d58ae870c79a001c0705a682fd22818210412
7
- data.tar.gz: 5058f511ec0e2707275605bee72d29d44c190e563e9b83da65e8b8619199186345c78231b35aa685fe183d6c0dd6f6996115059fd72c991c8d7babb39dfe7be8
6
+ metadata.gz: 67f1ea3177544e5c4e5472d2b1e81d894a42346313ae4666f53f9de89571b36a55d50ee247dae0b8af2dce869cec03ea72890009817ee613b7cab37b93bddbfe
7
+ data.tar.gz: 5bcc8fc0ab51a75032ed0a3d8d2e20dad830f9b6bb92f75841be0fa23e05f60d66e76cb7e23c5a8b17fffba0634213d3b4299fde0c86314858f396135d2a9a84
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.3.0]
2
+
3
+ Less folder nesting for the common case: most papers only ever have one version.
4
+
5
+ ### Breaking
6
+
7
+ - Single-version papers are archived flat again: a paper whose only archived version is v1 keeps its files directly in the paper folder, without a `v1/` level. `v<N>/` folders are used only when a paper has more than one version. Archiving a second version of a flat paper moves the existing files into `v<N>/` first, rewriting `html/` links into `_shared/` for the new depth. A paper whose latest version is v2 or later starts out in `v<N>/` folders. Existing 0.2.0 archives keep their `v1/` folders.
8
+
1
9
  ## [0.2.0]
2
10
 
3
11
  Versioned archives, author affiliations, and a round of robustness fixes. Two breaking changes to the output layout and metadata; see below.
data/README.md CHANGED
@@ -108,27 +108,40 @@ $ARXIV_DOWNLOAD_PATH/ # default: $HOME/Downloads/ArXiv_Papers
108
108
  arxiv.org/static/...
109
109
  cdn.jsdelivr.net/...
110
110
  YYYY/MM/DD/<primary_category>/<arxiv-id>-<slug>/
111
- v<N>/ # one folder per archived version
112
- <arxiv-id>v<N>.pdf
113
- <arxiv-id>v<N>-abstract.html
114
- metadata.md # YAML frontmatter + Markdown body
115
- metadata.yaml
116
- metadata.json
117
- metadata.bib # upstream BibTeX, falls back to synthesized
118
- html/ # absent when the paper has no HTML version
119
- <arxiv-id>v<N>.html # path-rewritten to local assets
120
- x1.png, x2.png, ... # paper-specific images
121
- src/ # absent for PDF-only submissions
122
- *.tex, *.bbl, ... # extracted from /src/<id>v<N>
111
+ <arxiv-id>v<N>.pdf
112
+ <arxiv-id>v<N>-abstract.html
113
+ metadata.md # YAML frontmatter + Markdown body
114
+ metadata.yaml
115
+ metadata.json
116
+ metadata.bib # upstream BibTeX, falls back to synthesized
117
+ html/ # absent when the paper has no HTML version
118
+ <arxiv-id>v<N>.html # path-rewritten to local assets
119
+ x1.png, x2.png, ... # paper-specific images
120
+ src/ # absent for PDF-only submissions
121
+ *.tex, *.bbl, ... # extracted from /src/<id>v<N>
123
122
  ```
124
123
 
125
- An unversioned ID (`2508.16190`) archives the latest version. A versioned ID (`2508.16190v1`) archives that version. Different versions of the same paper sit side by side under the same paper folder.
124
+ An unversioned ID (`2508.16190`) archives the latest version. A versioned ID (`2508.16190v1`) archives that version.
126
125
 
127
- Each version downloads into `v<N>.partial/` and is renamed to `v<N>/` only when every file succeeded. Re-running skips versions whose `v<N>/` already exists and retries interrupted ones from scratch.
126
+ A paper with a single archived version v1 is kept flat, as above. When a paper has more than one version, each version gets its own `v<N>/` folder with the same contents:
127
+
128
+ ```txt
129
+ YYYY/MM/DD/<primary_category>/<arxiv-id>-<slug>/
130
+ v1/
131
+ <arxiv-id>v1.pdf
132
+ ...
133
+ v2/
134
+ <arxiv-id>v2.pdf
135
+ ...
136
+ ```
137
+
138
+ Archiving a second version of a flat paper first moves the existing files into `v<N>/` (rewriting `html/` links into `_shared/` for the new depth). A paper whose latest version is v2 or later starts out in `v<N>/` folders.
139
+
140
+ Each version downloads into a sibling `.partial` folder and is renamed into place only when every file succeeded. Re-running skips versions already archived and retries interrupted ones from scratch.
128
141
 
129
142
  `YYYY/MM/DD` is the original submission date. `<primary_category>` is from the paper's metadata (`cs.CL`, `math.NT`, etc). `<slug>` is derived from the paper title (Unicode → ASCII, hyphenated, truncated to 80 chars at a word boundary).
130
143
 
131
- For legacy IDs containing `/` (e.g. `cs/0002001`), the slash is replaced with `-` in the directory name and file names (`cs-0002001-.../v1/cs-0002001v1.pdf`).
144
+ For legacy IDs containing `/` (e.g. `cs/0002001`), the slash is replaced with `-` in the directory name and file names (`cs-0002001-.../cs-0002001v1.pdf`).
132
145
 
133
146
  ## Library usage
134
147
 
@@ -138,7 +151,7 @@ require 'arxiv/downloader'
138
151
  identifier = Arxiv::Downloader::Identifier.new '2508.16190'
139
152
  client = Arxiv::Downloader::Client.new # 3-second rate limit by default
140
153
  path = Arxiv::Downloader::Archive.new(identifier, root: '/tmp/papers', client: client).run
141
- # => "/tmp/papers/2025/08/22/cs.CL/2508.16190-comicscene154-a-scene-dataset-for-comic-analysis/v1"
154
+ # => "/tmp/papers/2025/08/22/cs.CL/2508.16190-comicscene154-a-scene-dataset-for-comic-analysis"
142
155
  ```
143
156
 
144
157
  ## Development
@@ -9,12 +9,14 @@ module Arxiv
9
9
  @client = client
10
10
  end
11
11
 
12
- # Downloads into v<N>.partial/ and
13
- # renames it to v<N>/ only once everything succeeded.
14
- # An existing v<N>/ is always complete and is skipped.
12
+ # Downloads into <destination>.partial/ and
13
+ # renames it into place only once everything succeeded.
14
+ # An archived version is always complete and is skipped.
15
+ # PaperFolder decides flat (single version) vs v<N>/ (several versions).
15
16
  def run
16
- return paper_dir if Dir.exist? paper_dir
17
+ return paper_folder.location_of(metadata.version) if paper_folder.archived? metadata.version
17
18
 
19
+ paper_folder.unflatten!
18
20
  FileUtils.rm_rf staging_dir
19
21
  FileUtils.mkdir_p staging_dir
20
22
 
@@ -24,8 +26,8 @@ module Arxiv
24
26
  download_source_archive
25
27
  write_sidecars
26
28
 
27
- File.rename staging_dir, paper_dir
28
- paper_dir
29
+ File.rename staging_dir, destination
30
+ destination
29
31
  end
30
32
 
31
33
  private
@@ -44,13 +46,18 @@ module Arxiv
44
46
  @archived ||= Identifier.new "#{metadata.arxiv_id}v#{metadata.version}"
45
47
  end
46
48
 
47
- def paper_dir
48
- @paper_dir ||= File.join @root, Path.new(metadata).to_s, "v#{metadata.version}"
49
+ def paper_folder
50
+ @paper_folder ||= PaperFolder.new File.join(@root, Path.new(metadata).to_s)
49
51
  end
50
52
 
51
- # a sibling of paper_dir, so relative ../_shared/ refs survive the rename
53
+ # evaluated after unflatten!, which can turn a flat folder into v<N>/ folders
54
+ def destination
55
+ @destination ||= paper_folder.destination_for metadata.version
56
+ end
57
+
58
+ # a sibling of destination, so relative ../_shared/ refs survive the rename
52
59
  def staging_dir
53
- "#{paper_dir}.partial"
60
+ "#{destination}.partial"
54
61
  end
55
62
 
56
63
  def download_pdf
@@ -0,0 +1,85 @@
1
+ require 'fileutils'
2
+ require 'nokogiri'
3
+ require 'yaml'
4
+
5
+ module Arxiv
6
+ module Downloader
7
+ # One paper's folder. A paper with a single archived version is kept flat
8
+ # (files directly in the folder); once a second version arrives, each
9
+ # version gets its own v<N>/ folder.
10
+ class PaperFolder
11
+ VERSION_FOLDER = /\Av\d+(\.partial)?\z/
12
+
13
+ attr_reader :path
14
+
15
+ def initialize path
16
+ @path = path
17
+ end
18
+
19
+ def archived? version
20
+ Dir.exist? location_of(version)
21
+ end
22
+
23
+ def location_of version
24
+ return path if flat_version == version
25
+
26
+ version_path version
27
+ end
28
+
29
+ # where a new download of `version` goes; call unflatten! first if the folder is flat
30
+ def destination_for version
31
+ return path if version == 1 && !versioned?
32
+
33
+ version_path version
34
+ end
35
+
36
+ # moves the flat version into v<N>/ so another version can sit beside it
37
+ def unflatten!
38
+ return if flat_version.nil?
39
+
40
+ destination = version_path flat_version
41
+ FileUtils.mkdir_p destination
42
+
43
+ # metadata.yaml marks the folder as flat, so it moves last
44
+ children = Dir.children(path) - [File.basename(destination), metadata_filename]
45
+ children.each { FileUtils.mv File.join(path, it), destination }
46
+ deepen_shared_links File.join(destination, 'html')
47
+ FileUtils.mv File.join(path, metadata_filename), destination
48
+ end
49
+
50
+ private
51
+
52
+ def flat_version
53
+ metadata_path = File.join path, metadata_filename
54
+ return unless File.exist? metadata_path
55
+
56
+ ::YAML.safe_load_file(metadata_path, permitted_classes: [Date]).fetch 'version'
57
+ end
58
+
59
+ def versioned?
60
+ Dir.exist?(path) && Dir.children(path).any? { VERSION_FOLDER.match? it }
61
+ end
62
+
63
+ def version_path version
64
+ File.join path, "v#{version}"
65
+ end
66
+
67
+ def metadata_filename
68
+ Metadata::YAML::FILENAME
69
+ end
70
+
71
+ # html/ moved one level deeper, so relative links up into _shared/ need one more ../
72
+ def deepen_shared_links html_dir
73
+ Dir.glob(File.join(html_dir, '*.html')).each do |html_path|
74
+ document = Nokogiri::HTML File.read(html_path)
75
+ HTMLArchive::ASSET_SELECTORS.each do |selector, attribute|
76
+ document.css(selector).each do |node|
77
+ node[attribute] = "../#{node[attribute]}" if node[attribute].start_with? '../'
78
+ end
79
+ end
80
+ File.write html_path, document.to_html
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,5 +1,5 @@
1
1
  module Arxiv
2
2
  module Downloader
3
- VERSION = '0.2.0'.freeze
3
+ VERSION = '0.3.0'.freeze
4
4
  end
5
5
  end
@@ -16,6 +16,7 @@ require_relative 'downloader/metadata/bibtex' # after metadata
16
16
  require_relative 'downloader/metadata/json' # after metadata
17
17
  require_relative 'downloader/metadata/markdown' # after metadata
18
18
  require_relative 'downloader/metadata/yaml' # after metadata
19
+ require_relative 'downloader/paper_folder'
19
20
  require_relative 'downloader/paper_not_found' # after error
20
21
  require_relative 'downloader/path'
21
22
  require_relative 'downloader/pdf'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arxiv-dl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Becker
@@ -116,6 +116,7 @@ files:
116
116
  - lib/arxiv/downloader/metadata/json.rb
117
117
  - lib/arxiv/downloader/metadata/markdown.rb
118
118
  - lib/arxiv/downloader/metadata/yaml.rb
119
+ - lib/arxiv/downloader/paper_folder.rb
119
120
  - lib/arxiv/downloader/paper_not_found.rb
120
121
  - lib/arxiv/downloader/path.rb
121
122
  - lib/arxiv/downloader/pdf.rb