A truncated debrid download (in-memory byte counter hit 100% while the
NFS write-back silently dropped most of the bytes) was marked completed.
The 1.1.6 fsync fix closed the debrid-specific hole; this generalizes the
guarantee so "completed" never means a corrupt file on ANY backend.
- IntegrityError + bounded retry: on a corrupt/short result the manager
re-downloads the same source up to 3x (clean start), then surfaces the
task as damaged ("corrupt download:" prefix) instead of completing it.
- verify (size mismatch / empty), debrid (incomplete / post-write / flush),
torrent (BytesMissing), usenet (par2 unrepairable / repair-failed) all
classify integrity failures so they route through the retry/damaged path.
- scanner: a file ffprobe can't read is emitted as a damaged library_item
(reason "unreadable") instead of being silently dropped from the sync.
- tests: manager retry-then-success + retry-exhausted-then-damaged,
verifying->resolving transition, damaged sync item.
122 lines
3.4 KiB
Go
122 lines
3.4 KiB
Go
package library
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/torrentclaw/unarr/internal/library/mediainfo"
|
|
)
|
|
|
|
func TestBuildSyncItems(t *testing.T) {
|
|
cache := &LibraryCache{
|
|
Items: []LibraryItem{
|
|
{
|
|
FilePath: "/media/movies/Inception.mkv",
|
|
FileName: "Inception.2010.1080p.mkv",
|
|
FileSize: 5000000000,
|
|
Title: "Inception",
|
|
Year: "2010",
|
|
MediaInfo: &mediainfo.MediaInfo{
|
|
Video: &mediainfo.VideoInfo{
|
|
Codec: "hevc",
|
|
Width: 1920,
|
|
Height: 1080,
|
|
BitDepth: 10,
|
|
HDR: "HDR10",
|
|
},
|
|
Audio: []mediainfo.AudioTrack{
|
|
{Lang: "en", Codec: "ac3", Channels: 6, Default: true},
|
|
{Lang: "es", Codec: "aac", Channels: 2},
|
|
},
|
|
Subtitles: []mediainfo.SubtitleTrack{
|
|
{Lang: "en", Codec: "subrip"},
|
|
{Lang: "es", Codec: "subrip"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
FilePath: "/media/shows/Breaking.Bad.S01E01.mkv",
|
|
FileName: "Breaking.Bad.S01E01.mkv",
|
|
FileSize: 1000000000,
|
|
Title: "Breaking Bad",
|
|
Season: 1,
|
|
Episode: 1,
|
|
},
|
|
{
|
|
// Item with scan error — should be skipped
|
|
FilePath: "/media/bad.mkv",
|
|
FileName: "bad.mkv",
|
|
ScanError: "ffprobe failed",
|
|
},
|
|
},
|
|
}
|
|
|
|
items := BuildSyncItems(cache)
|
|
|
|
// 3 items: the movie, the show, and the scan-error file surfaced as DAMAGED
|
|
// (no longer silently dropped — the web flags it for re-download).
|
|
if len(items) != 3 {
|
|
t.Fatalf("expected 3 items (1 damaged), got %d", len(items))
|
|
}
|
|
|
|
// First item: movie with full media info
|
|
movie := items[0]
|
|
if movie.Title != "Inception" {
|
|
t.Errorf("title = %q, want Inception", movie.Title)
|
|
}
|
|
if movie.ContentType != "movie" {
|
|
t.Errorf("contentType = %q, want movie", movie.ContentType)
|
|
}
|
|
if movie.Resolution != "1080p" {
|
|
t.Errorf("resolution = %q, want 1080p", movie.Resolution)
|
|
}
|
|
if movie.VideoCodec != "hevc" {
|
|
t.Errorf("videoCodec = %q, want hevc", movie.VideoCodec)
|
|
}
|
|
if movie.HDR != "HDR10" {
|
|
t.Errorf("hdr = %q, want HDR10", movie.HDR)
|
|
}
|
|
if movie.AudioCodec != "ac3" {
|
|
t.Errorf("audioCodec = %q, want ac3", movie.AudioCodec)
|
|
}
|
|
if movie.AudioChannels != 6 {
|
|
t.Errorf("audioChannels = %d, want 6", movie.AudioChannels)
|
|
}
|
|
if len(movie.AudioLanguages) != 2 {
|
|
t.Errorf("audioLanguages count = %d, want 2", len(movie.AudioLanguages))
|
|
}
|
|
if len(movie.SubtitleLanguages) != 2 {
|
|
t.Errorf("subtitleLanguages count = %d, want 2", len(movie.SubtitleLanguages))
|
|
}
|
|
|
|
// Second item: show without media info
|
|
show := items[1]
|
|
if show.ContentType != "show" {
|
|
t.Errorf("contentType = %q, want show", show.ContentType)
|
|
}
|
|
if show.Season != 1 || show.Episode != 1 {
|
|
t.Errorf("season/episode = %d/%d, want 1/1", show.Season, show.Episode)
|
|
}
|
|
if show.Resolution != "" {
|
|
t.Errorf("resolution should be empty, got %q", show.Resolution)
|
|
}
|
|
|
|
// Third item: scan-error file surfaced as damaged (unreadable), not skipped.
|
|
damaged := items[2]
|
|
if damaged.FilePath != "/media/bad.mkv" {
|
|
t.Errorf("damaged filePath = %q, want /media/bad.mkv", damaged.FilePath)
|
|
}
|
|
if damaged.Integrity != "damaged" {
|
|
t.Errorf("integrity = %q, want damaged", damaged.Integrity)
|
|
}
|
|
if damaged.IntegrityReason != "unreadable" {
|
|
t.Errorf("integrityReason = %q, want unreadable", damaged.IntegrityReason)
|
|
}
|
|
}
|
|
|
|
func TestBuildSyncItemsEmpty(t *testing.T) {
|
|
cache := &LibraryCache{Items: nil}
|
|
items := BuildSyncItems(cache)
|
|
if len(items) != 0 {
|
|
t.Errorf("expected 0 items, got %d", len(items))
|
|
}
|
|
}
|