*перезапись тэгов на месте: файл сохраняет inode (Audiobookshelf опознаёт файлы по inode, замена через mv путала его библиотеку)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e686ba304
commit
a6b4f0fbcf
2 files changed
+70
-21
No files matched your search
+50
-21
@@ -558,30 +558,68 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запись тэгов: новый тэг + аудио из исходного файла, через временный файл.
|
// Запись тэгов: новый тэг + аудио из исходного файла собираются во временном файле,
|
||||||
|
// затем копируются обратно в исходный. Файл остаётся тем же (inode, владелец, права):
|
||||||
|
// Audiobookshelf опознаёт файлы по inode, и замена файла новым путает его библиотеку.
|
||||||
// Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало
|
// Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало
|
||||||
fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) ! {
|
fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) ! {
|
||||||
parsed := parse_id3v2(path)!
|
parsed := parse_id3v2(path)!
|
||||||
tag := build_tag(parsed, t, opts)
|
tag := build_tag(parsed, t, opts)
|
||||||
|
|
||||||
// Владелец и права исходного файла: перезапись через временный файл иначе их потеряет
|
|
||||||
// (на NAS файлы библиотеки принадлежат пользователю приложения, а агент может работать от root)
|
|
||||||
orig := os.stat(path) or { os.Stat{} }
|
|
||||||
tmp_path := path + '.tagtmp'
|
tmp_path := path + '.tagtmp'
|
||||||
|
mut buf := []u8{len: 1024 * 1024}
|
||||||
|
{
|
||||||
mut src := os.open(path)!
|
mut src := os.open(path)!
|
||||||
mut dst := os.create(tmp_path)!
|
|
||||||
mut ok := false
|
|
||||||
defer {
|
defer {
|
||||||
src.close()
|
src.close()
|
||||||
|
}
|
||||||
|
mut dst := os.create(tmp_path)!
|
||||||
|
defer {
|
||||||
dst.close()
|
dst.close()
|
||||||
if !ok {
|
}
|
||||||
|
dst.write(tag) or {
|
||||||
os.rm(tmp_path) or {}
|
os.rm(tmp_path) or {}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
dst.write(tag)!
|
|
||||||
src.seek(i64(parsed.audio_start), .start)!
|
src.seek(i64(parsed.audio_start), .start)!
|
||||||
mut buf := []u8{len: 1024 * 1024}
|
for {
|
||||||
|
n := src.read(mut buf) or { 0 }
|
||||||
|
if n <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dst.write(buf[..n]) or {
|
||||||
|
os.rm(tmp_path) or {}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy_back(tmp_path, path, mut buf) or {
|
||||||
|
// Исходный файл мог остаться наполовину переписанным — полная копия во временном
|
||||||
|
orig := os.stat(path) or { os.Stat{} }
|
||||||
|
os.mv(tmp_path, path, overwrite: true) or {
|
||||||
|
return error('${err}; новое содержимое сохранено в ${tmp_path}')
|
||||||
|
}
|
||||||
|
if orig.mode != 0 {
|
||||||
|
os.chmod(path, int(orig.mode & 0o7777)) or {}
|
||||||
|
$if !windows {
|
||||||
|
os.chown(path, int(orig.uid), int(orig.gid)) or {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.rm(tmp_path) or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Содержимое from — в существующий файл to (с обрезкой), не заменяя сам файл
|
||||||
|
fn copy_back(from string, to string, mut buf []u8) ! {
|
||||||
|
mut src := os.open(from)!
|
||||||
|
defer {
|
||||||
|
src.close()
|
||||||
|
}
|
||||||
|
mut dst := os.open_file(to, 'w')!
|
||||||
|
defer {
|
||||||
|
dst.close()
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
n := src.read(mut buf) or { 0 }
|
n := src.read(mut buf) or { 0 }
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
@@ -589,16 +627,7 @@ fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) ! {
|
|||||||
}
|
}
|
||||||
dst.write(buf[..n])!
|
dst.write(buf[..n])!
|
||||||
}
|
}
|
||||||
src.close()
|
dst.flush()
|
||||||
dst.close()
|
|
||||||
ok = true
|
|
||||||
if orig.mode != 0 {
|
|
||||||
os.chmod(tmp_path, int(orig.mode & 0o7777)) or {}
|
|
||||||
$if !windows {
|
|
||||||
os.chown(tmp_path, int(orig.uid), int(orig.gid)) or {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.mv(tmp_path, path, overwrite: true)!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- публичный API с защитой от паники ----------
|
// ---------- публичный API с защитой от паники ----------
|
||||||
|
|||||||
@@ -131,3 +131,23 @@ fn test_write_keeps_mode() {
|
|||||||
assert after.mode & 0o777 == before.mode & 0o777
|
assert after.mode & 0o777 == before.mode & 0o777
|
||||||
assert after.uid == before.uid
|
assert after.uid == before.uid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Файл остаётся тем же: Audiobookshelf опознаёт файлы по inode
|
||||||
|
fn test_write_keeps_inode() {
|
||||||
|
path := tmp_copy('test_id3v23.mp3')
|
||||||
|
defer {
|
||||||
|
os.rm(path) or {}
|
||||||
|
}
|
||||||
|
before := os.stat(path) or { panic(err) }
|
||||||
|
metadata.write_book_tags(path, metadata.BookTags{
|
||||||
|
title: 'Тот же файл'
|
||||||
|
comment: 'длинное описание '.repeat(500)
|
||||||
|
}) or { panic(err) }
|
||||||
|
after := os.stat(path) or { panic(err) }
|
||||||
|
$if !windows {
|
||||||
|
assert after.inode == before.inode
|
||||||
|
}
|
||||||
|
assert !os.exists(path + '.tagtmp')
|
||||||
|
t := metadata.read_book_tags(path) or { panic(err) }
|
||||||
|
assert t.tags.title == 'Тот же файл'
|
||||||
|
}
|
||||||
Reference in new issue
Block a user