caesura 0.30.2

An all-in-one command line tool to transcode FLAC audio files and upload to gazelle based indexers/trackers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
use crate::testing_prelude::*;

/// Delay between file operations to ensure filesystem modification times differ.
///
/// When verifying that files were not recreated, we compare modification times before and after
/// an operation. This delay ensures that if a file were recreated, it would have a detectably
/// different modification time. The filesystem's timestamp resolution varies by OS and filesystem
/// (e.g., ext4 has ~1ms resolution, NTFS ~100ns, but some systems round to seconds).
const MODIFICATION_TIME_WAIT: Duration = Duration::from_millis(50);

/// Test that transcode only creates the tracker-specific torrent file.
#[tokio::test]
async fn transcode_creates_only_indexed_torrent() {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let provider = host.services.get_required::<SourceProvider>();
    let transcoder = host.services.get_required::<TranscodeCommand>();
    let paths = host.services.get_required::<PathManager>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    // Act
    let result = transcoder.execute(&source).await;

    // Assert
    assert!(result.is_ok(), "transcode should succeed");
    let indexed_torrent = paths.get_torrent_path(&source, TargetFormat::_320);
    assert!(
        indexed_torrent.exists(),
        "Indexed torrent should exist: {}",
        indexed_torrent.display()
    );
    let filename = indexed_torrent
        .file_name()
        .expect("should have filename")
        .to_string_lossy();
    assert!(
        filename.ends_with(".red.torrent"),
        "Torrent should have indexer suffix: {filename}"
    );
}

/// Test that `get_or_duplicate_existing_torrent_path` returns path when it exists.
#[tokio::test]
async fn get_or_duplicate_returns_path_when_exists() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let provider = host.services.get_required::<SourceProvider>();
    let transcoder = host.services.get_required::<TranscodeCommand>();
    let paths = host.services.get_required::<PathManager>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    transcoder
        .execute(&source)
        .await
        .expect("transcode should succeed");

    // Act
    let result = paths
        .get_or_duplicate_existing_torrent_path(&source, TargetFormat::_320)
        .await?;

    // Assert
    assert!(result.is_some(), "Should find indexed torrent");
    let path = result.expect("checked above");
    let filename = path
        .file_name()
        .expect("should have name")
        .to_string_lossy();
    assert!(
        filename.ends_with(".red.torrent"),
        "Should return indexed path: {filename}"
    );

    Ok(())
}

/// Test that `get_or_duplicate` creates torrent from another tracker's torrent.
#[tokio::test]
async fn get_or_duplicate_creates_from_other_tracker() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    // First create a transcode with RED indexer
    let host_red = HostBuilder::new()
        .with_mock_api(album.clone())
        .with_test_options(&test_dir)
        .await
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let provider = host_red.services.get_required::<SourceProvider>();
    let transcoder = host_red.services.get_required::<TranscodeCommand>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    transcoder
        .execute(&source)
        .await
        .expect("transcode should succeed");

    // Now create a new host with OPS indexer, using same output directory
    let host_ops = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: test_dir.output(),
            indexer: "ops".to_owned(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let paths_ops = host_ops.services.get_required::<PathManager>();

    // Act - should find .red.torrent and duplicate to .ops.torrent
    let result = paths_ops
        .get_or_duplicate_existing_torrent_path(&source, TargetFormat::_320)
        .await?;

    // Assert
    assert!(result.is_some(), "Should create from RED torrent");
    let path = result.expect("checked above");
    let filename = path
        .file_name()
        .expect("should have name")
        .to_string_lossy();
    assert!(
        filename.ends_with(".ops.torrent"),
        "Should create OPS torrent: {filename}"
    );
    assert!(path.exists(), "OPS torrent file should exist");

    Ok(())
}

/// Test that `get_or_duplicate` returns None when no torrent exists.
#[tokio::test]
async fn get_or_duplicate_returns_none_when_missing() -> Result<(), TestError> {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .expect_build();
    let provider = host.services.get_required::<SourceProvider>();
    let paths = host.services.get_required::<PathManager>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    // Act - no transcode performed, no torrent files exist
    let result = paths
        .get_or_duplicate_existing_torrent_path(&source, TargetFormat::_320)
        .await?;

    // Assert
    assert!(
        result.is_none(),
        "Should return None when no torrent exists"
    );

    Ok(())
}

/// Test that torrent filename includes format and indexer.
#[tokio::test]
async fn torrent_filename_includes_format_and_indexer() {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let provider = host.services.get_required::<SourceProvider>();
    let transcoder = host.services.get_required::<TranscodeCommand>();
    let paths = host.services.get_required::<PathManager>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    // Act
    transcoder
        .execute(&source)
        .await
        .expect("transcode should succeed");

    // Assert
    let torrent_path = paths.get_torrent_path(&source, TargetFormat::_320);
    let filename = torrent_path
        .file_name()
        .expect("should have filename")
        .to_string_lossy();
    assert!(
        filename.contains("[WEB 320]"),
        "Filename should contain format: {filename}"
    );
    assert!(
        filename.ends_with(".red.torrent"),
        "Filename should end with indexer: {filename}"
    );
    assert!(
        filename.contains("Test Artist"),
        "Filename should contain artist: {filename}"
    );
}

/// Test that multiple target formats each get their own torrent file.
#[tokio::test]
async fn transcode_creates_torrents_for_each_format() {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320, TargetFormat::V0],
            ..TargetOptions::default()
        })
        .expect_build();
    let provider = host.services.get_required::<SourceProvider>();
    let transcoder = host.services.get_required::<TranscodeCommand>();
    let paths = host.services.get_required::<PathManager>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    // Act
    let result = transcoder.execute(&source).await;

    // Assert
    assert!(result.is_ok(), "transcode should succeed");
    for target in [TargetFormat::_320, TargetFormat::V0] {
        let torrent_path = paths.get_torrent_path(&source, target);
        assert!(
            torrent_path.exists(),
            "{target} torrent should exist: {}",
            torrent_path.display()
        );
    }
}

/// Test that switching indexer finds existing torrent and skips transcoding.
#[tokio::test]
async fn transcode_skips_when_other_tracker_torrent_exists() {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    // First create a transcode with RED indexer
    let host_red = HostBuilder::new()
        .with_mock_api(album.clone())
        .with_test_options(&test_dir)
        .await
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let provider = host_red.services.get_required::<SourceProvider>();
    let transcoder_red = host_red.services.get_required::<TranscodeCommand>();
    let paths_red = host_red.services.get_required::<PathManager>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    transcoder_red
        .execute(&source)
        .await
        .expect("transcode should succeed");

    let red_torrent = paths_red.get_torrent_path(&source, TargetFormat::_320);
    assert!(red_torrent.exists(), "RED torrent should exist");

    // Record modification time of a transcoded file to verify no re-transcoding
    let transcode_dir = paths_red.get_transcode_target_dir(&source, TargetFormat::_320);
    let mp3_file = read_dir(&transcode_dir)
        .expect("should read transcode dir")
        .filter_map(Result::ok)
        .find(|e| e.path().extension().is_some_and(|ext| ext == "mp3"))
        .expect("should have mp3 file");
    let mp3_modified_before = metadata(mp3_file.path())
        .expect("should get metadata")
        .modified()
        .expect("should get mtime");
    sleep(MODIFICATION_TIME_WAIT).await;

    // Now create a new host with OPS indexer
    let host_ops = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(SharedOptions {
            content: vec![SAMPLE_SOURCES_DIR.clone()],
            output: test_dir.output(),
            indexer: "ops".to_owned(),
            ..SharedOptions::mock()
        })
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let transcoder_ops = host_ops.services.get_required::<TranscodeCommand>();
    let paths_ops = host_ops.services.get_required::<PathManager>();

    // Act - transcode with OPS should find RED torrent and skip
    transcoder_ops
        .execute(&source)
        .await
        .expect("transcode should succeed");
    let ops_torrent = paths_ops.get_torrent_path(&source, TargetFormat::_320);
    assert!(
        ops_torrent.exists(),
        "OPS torrent should be created from RED: {}",
        ops_torrent.display()
    );
    assert!(red_torrent.exists(), "RED torrent should still exist");

    // Verify transcoding was skipped by checking mp3 wasn't modified
    let mp3_modified_after = metadata(mp3_file.path())
        .expect("should get metadata")
        .modified()
        .expect("should get mtime");
    assert_eq!(
        mp3_modified_before, mp3_modified_after,
        "MP3 file should not be recreated - transcoding should be skipped"
    );
}

/// Test that re-running transcode skips when torrent exists.
#[tokio::test]
async fn transcode_skips_when_torrent_exists() {
    // Arrange
    init_logger();
    let test_dir = TestDirectory::new();
    let album = AlbumProvider::get(SampleFormat::default()).await;
    let host = HostBuilder::new()
        .with_mock_api(album)
        .with_test_options(&test_dir)
        .await
        .with_options(TargetOptions {
            target: vec![TargetFormat::_320],
            ..TargetOptions::default()
        })
        .expect_build();
    let provider = host.services.get_required::<SourceProvider>();
    let transcoder = host.services.get_required::<TranscodeCommand>();
    let paths = host.services.get_required::<PathManager>();
    let source = provider
        .get(AlbumConfig::TORRENT_ID)
        .await
        .expect("should not fail")
        .expect("should find source");

    // First transcode
    transcoder
        .execute(&source)
        .await
        .expect("transcode should succeed");

    let torrent_path = paths.get_torrent_path(&source, TargetFormat::_320);
    let modified_before = metadata(&torrent_path)
        .expect("torrent should exist")
        .modified()
        .expect("should get modified time");
    sleep(MODIFICATION_TIME_WAIT).await;

    // Act - second transcode should skip
    transcoder
        .execute(&source)
        .await
        .expect("transcode should succeed");
    let modified_after = metadata(&torrent_path)
        .expect("torrent should exist")
        .modified()
        .expect("should get modified time");
    assert_eq!(
        modified_before, modified_after,
        "Torrent file should not be recreated"
    );
}