feat: can now download m3u8 videos

This commit is contained in:
kodjomoustapha 2024-09-06 18:59:16 +01:00
parent 3bf1d08e0e
commit 8ed6b21125
14 changed files with 271 additions and 70 deletions

View file

@ -41,11 +41,13 @@ class AnimePlayerView extends riv.ConsumerStatefulWidget {
class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
String? _infoHash;
HttpServer? _httpServer;
@override
void dispose() {
if (_infoHash != null) {
MTorrentServer().removeTorrent(_infoHash);
}
_httpServer?.close();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
super.dispose();
@ -58,8 +60,9 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
return serversData.when(
data: (data) {
final (videos, isLocal, infoHash) = data;
final (videos, isLocal, infoHash, httpServer) = data;
_infoHash = infoHash;
_httpServer = httpServer;
if (videos.isEmpty &&
!(widget.episode.manga.value!.isLocalArchive ?? false)) {
return Scaffold(

View file

@ -13,6 +13,7 @@ import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/services/get_video_list.dart';
import 'package:mangayomi/services/get_chapter_pages.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/services/m3u8/m3u8_downloader.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/headers.dart';
import 'package:mangayomi/utils/reg_exp_matcher.dart';
@ -47,7 +48,9 @@ Future<List<PageUrl>> downloadChapter(
"downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceForbiddenCharacters('_')}${isManga ? "/$scanlator${chapter.name!.replaceForbiddenCharacters('_')}" : ""}";
path = Directory("${path1!.path}$finalPath/");
Map<String, String> videoHeader = {};
bool hasM3U8File = false;
bool nonM3U8File = false;
M3u8Downloader? m3u8Downloader;
void savePageUrls() {
final settings = isar.settings.getSync(227)!;
List<ChapterPageurls>? chapterPageUrls = [];
@ -82,15 +85,36 @@ Future<List<PageUrl>> downloadChapter(
});
} else {
ref
.read(getVideoListProvider(
episode: chapter,
).future)
.then((value) {
final videosUrls = value.$1
.read(
getVideoListProvider(episode: chapter, ignoreM3u8File: true).future)
.then((value) async {
final m3u8Urls = value.$1
.where((element) =>
element.originalUrl.endsWith(".m3u8") ||
element.originalUrl.endsWith(".m3u"))
.toList();
final nonM3u8Urls = value.$1
.where((element) => element.originalUrl.isMediaVideo())
.toList();
nonM3U8File = nonM3u8Urls.isNotEmpty && !Platform.isIOS;
hasM3U8File = nonM3U8File ? false : m3u8Urls.isNotEmpty;
final videosUrls = nonM3U8File
? nonM3u8Urls
: (hasM3U8File || Platform.isIOS)
? m3u8Urls
: nonM3u8Urls;
if (videosUrls.isNotEmpty) {
pageUrls = [PageUrl(videosUrls.first.url)];
List<TsInfo> tsList = [];
if (hasM3U8File) {
m3u8Downloader = M3u8Downloader(
m3u8Url: videosUrls.first.url,
downloadDir: "${path!.path}$chapterName",
headers: videosUrls.first.headers ?? {});
tsList = await m3u8Downloader!.getTsList();
}
pageUrls = hasM3U8File
? [...tsList.map((e) => PageUrl(e.url))]
: [PageUrl(videosUrls.first.url)];
videoHeader.addAll(videosUrls.first.headers ?? {});
isOk = true;
}
@ -202,6 +226,28 @@ Future<List<PageUrl>> downloadChapter(
if (file.existsSync()) {
await file.copy("${path.path}$chapterName.mp4");
await file.delete();
} else if (hasM3U8File) {
final tempFile = File(
"${tempDir.path}/Mangayomi/$finalPath/$chapterName/TS_${index + 1}.ts");
final file = File("${path.path}$chapterName/TS_${index + 1}.ts");
if (tempFile.existsSync()) {
await tempFile
.copy("${path.path}$chapterName/TS_${index + 1}.ts");
await tempFile.delete();
} else if (file.existsSync()) {
} else {
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "TS_${index + 1}.ts",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath/$chapterName/',
updates: Updates.statusAndProgress,
allowPause: true,
retries: 3,
requiresWiFi: onlyOnWifi));
}
} else {
if ((await path.exists())) {
if (await File("${path.path}$chapterName.mp4").exists()) {
@ -259,13 +305,19 @@ Future<List<PageUrl>> downloadChapter(
await FileDownloader().downloadBatch(
tasks,
batchProgressCallback: (succeeded, failed) async {
if (isManga) {
if (isManga || hasM3U8File) {
if (succeeded == tasks.length) {
savePageUrls();
if (ref.watch(saveAsCBZArchiveStateProvider)) {
await ref.watch(convertToCBZProvider(path!.path, mangaDir.path,
chapter.name!, pageUrls.map((e) => e.url).toList())
.future);
if (hasM3U8File) {
} else {
savePageUrls();
if (ref.watch(saveAsCBZArchiveStateProvider)) {
await ref.watch(convertToCBZProvider(
path!.path,
mangaDir.path,
chapter.name!,
pageUrls.map((e) => e.url).toList())
.future);
}
}
}
bool isEmpty = isar.downloads
@ -301,7 +353,7 @@ Future<List<PageUrl>> downloadChapter(
},
taskProgressCallback: (taskProgress) async {
final progress = taskProgress.progress;
if (!isManga) {
if (!isManga && !hasM3U8File) {
bool isEmpty = isar.downloads
.filter()
.chapterIdEqualTo(chapter.id!)
@ -335,7 +387,8 @@ Future<List<PageUrl>> downloadChapter(
if (progress == 1.0) {
final file = File(
"${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}");
await file.copy("${path!.path}${taskProgress.task.filename}");
await file.copy(
"${path!.path}${hasM3U8File ? "$chapterName/" : ""}${taskProgress.task.filename}");
await file.delete();
}
},

View file

@ -6,7 +6,7 @@ part of 'download_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$downloadChapterHash() => r'eca8ccbe5f93f07c3471af81355fe9b3a8ec11e8';
String _$downloadChapterHash() => r'ceb6f5d311f5da585b0272a0af598532ab511adc';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'aniskip.dart';
// RiverpodGenerator
// **************************************************************************
String _$aniSkipHash() => r'04cf38b827f60d846b1d8fe87e994e9876d106ff';
String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c';
/// See also [AniSkip].
@ProviderFor(AniSkip)

View file

@ -6,6 +6,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/video.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/services/m3u8/m3u8_server.dart';
import 'package:mangayomi/services/torrent_server.dart';
import 'package:mangayomi/utils/utils.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
@ -13,20 +14,27 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_video_list.g.dart';
@riverpod
Future<(List<Video>, bool, String?)> getVideoList(
GetVideoListRef ref, {
required Chapter episode,
}) async {
Future<(List<Video>, bool, String?, HttpServer?)> getVideoList(
GetVideoListRef ref,
{required Chapter episode,
bool ignoreM3u8File = false}) async {
final storageProvider = StorageProvider();
final mangaDirectory = await storageProvider.getMangaMainDirectory(episode);
final isLocalArchive = episode.manga.value!.isLocalArchive! &&
episode.manga.value!.source != "torrent";
final mp4animePath =
"${mangaDirectory!.path}${episode.name!.replaceForbiddenCharacters(' ')}.mp4";
final episodeFolderPath =
"${mangaDirectory.path}${episode.name!.replaceForbiddenCharacters(' ')}";
if (await File(mp4animePath).exists() || isLocalArchive) {
final path = isLocalArchive ? episode.archivePath : mp4animePath;
return ([Video(path!, episode.name!, path, subtitles: [])], true, null);
return (
[Video(path!, episode.name!, path, subtitles: [])],
true,
null,
null
);
}
final source =
getSource(episode.manga.value!.lang!, episode.manga.value!.source!);
@ -34,7 +42,19 @@ Future<(List<Video>, bool, String?)> getVideoList(
if (source?.isTorrent ?? false || episode.manga.value!.source == "torrent") {
final (videos, infohash) = await MTorrentServer()
.getTorrentPlaylist(episode.url, episode.archivePath);
return (videos, false, infohash);
return (videos, false, infohash, null);
}
if (File("$episodeFolderPath/index.m3u8").existsSync() && !ignoreM3u8File) {
const indexUrl = "http://localhost:3000/index.m3u8";
final httpServer = await m3u8Server(
m3u8Content: File("$episodeFolderPath/index.m3u8").readAsStringSync(),
episodeFolderPath: episodeFolderPath);
return (
[Video(indexUrl, episode.name!, indexUrl)],
false,
null,
httpServer
);
}
List<Video> list = [];
if (source?.sourceCodeLanguage == SourceCodeLanguage.dart) {
@ -48,5 +68,5 @@ Future<(List<Video>, bool, String?)> getVideoList(
videos.add(video);
}
}
return (videos, false, null);
return (videos, false, null, null);
}

View file

@ -6,7 +6,7 @@ part of 'get_video_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$getVideoListHash() => r'46918505b5cf3401ea9f41a5c287f8746b93b1b8';
String _$getVideoListHash() => r'2002f381edbe8c3c5e8a00826b3d9aaf49410e57';
/// Copied from Dart SDK
class _SystemHash {
@ -35,16 +35,18 @@ const getVideoListProvider = GetVideoListFamily();
/// See also [getVideoList].
class GetVideoListFamily
extends Family<AsyncValue<(List<Video>, bool, String?)>> {
extends Family<AsyncValue<(List<Video>, bool, String?, HttpServer?)>> {
/// See also [getVideoList].
const GetVideoListFamily();
/// See also [getVideoList].
GetVideoListProvider call({
required Chapter episode,
bool ignoreM3u8File = false,
}) {
return GetVideoListProvider(
episode: episode,
ignoreM3u8File: ignoreM3u8File,
);
}
@ -54,6 +56,7 @@ class GetVideoListFamily
) {
return call(
episode: provider.episode,
ignoreM3u8File: provider.ignoreM3u8File,
);
}
@ -73,15 +76,17 @@ class GetVideoListFamily
}
/// See also [getVideoList].
class GetVideoListProvider
extends AutoDisposeFutureProvider<(List<Video>, bool, String?)> {
class GetVideoListProvider extends AutoDisposeFutureProvider<
(List<Video>, bool, String?, HttpServer?)> {
/// See also [getVideoList].
GetVideoListProvider({
required Chapter episode,
bool ignoreM3u8File = false,
}) : this._internal(
(ref) => getVideoList(
ref as GetVideoListRef,
episode: episode,
ignoreM3u8File: ignoreM3u8File,
),
from: getVideoListProvider,
name: r'getVideoListProvider',
@ -93,6 +98,7 @@ class GetVideoListProvider
allTransitiveDependencies:
GetVideoListFamily._allTransitiveDependencies,
episode: episode,
ignoreM3u8File: ignoreM3u8File,
);
GetVideoListProvider._internal(
@ -103,13 +109,16 @@ class GetVideoListProvider
required super.debugGetCreateSourceHash,
required super.from,
required this.episode,
required this.ignoreM3u8File,
}) : super.internal();
final Chapter episode;
final bool ignoreM3u8File;
@override
Override overrideWith(
FutureOr<(List<Video>, bool, String?)> Function(GetVideoListRef provider)
FutureOr<(List<Video>, bool, String?, HttpServer?)> Function(
GetVideoListRef provider)
create,
) {
return ProviderOverride(
@ -122,43 +131,51 @@ class GetVideoListProvider
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
episode: episode,
ignoreM3u8File: ignoreM3u8File,
),
);
}
@override
AutoDisposeFutureProviderElement<(List<Video>, bool, String?)>
AutoDisposeFutureProviderElement<(List<Video>, bool, String?, HttpServer?)>
createElement() {
return _GetVideoListProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is GetVideoListProvider && other.episode == episode;
return other is GetVideoListProvider &&
other.episode == episode &&
other.ignoreM3u8File == ignoreM3u8File;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, episode.hashCode);
hash = _SystemHash.combine(hash, ignoreM3u8File.hashCode);
return _SystemHash.finish(hash);
}
}
mixin GetVideoListRef
on AutoDisposeFutureProviderRef<(List<Video>, bool, String?)> {
on AutoDisposeFutureProviderRef<(List<Video>, bool, String?, HttpServer?)> {
/// The parameter `episode` of this provider.
Chapter get episode;
/// The parameter `ignoreM3u8File` of this provider.
bool get ignoreM3u8File;
}
class _GetVideoListProviderElement
extends AutoDisposeFutureProviderElement<(List<Video>, bool, String?)>
with GetVideoListRef {
class _GetVideoListProviderElement extends AutoDisposeFutureProviderElement<
(List<Video>, bool, String?, HttpServer?)> with GetVideoListRef {
_GetVideoListProviderElement(super.provider);
@override
Chapter get episode => (origin as GetVideoListProvider).episode;
@override
bool get ignoreM3u8File => (origin as GetVideoListProvider).ignoreM3u8File;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,64 @@
import 'dart:io';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:path/path.dart' as path;
class TsInfo {
final String name;
final String url;
TsInfo(this.name, this.url);
}
class M3u8Downloader {
final String m3u8Url;
final String downloadDir;
final Map<String, String>? headers;
M3u8Downloader(
{required this.m3u8Url,
required this.downloadDir,
required this.headers});
Future<List<TsInfo>> getTsList() async {
final uri = Uri.parse(m3u8Url);
final m3u8Host = "${uri.scheme}://${uri.host}${path.dirname(uri.path)}";
final m3u8Body = await _getM3u8Body(m3u8Url, headers);
final tsList = _parseTsList(m3u8Host, m3u8Body);
if (kDebugMode) {
print("Total TS files to download: ${tsList.length}");
}
return tsList;
}
Future<String> _getM3u8Body(String url, Map<String, String>? headers) async {
final response =
await MClient.httpClient().get(Uri.parse(url), headers: headers);
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception("Failed to load m3u8 body");
}
}
List<TsInfo> _parseTsList(String host, String body) {
final lines = body.split("\n");
final tsList = <TsInfo>[];
int index = 0;
String allText = "";
for (final line in lines) {
if (!line.startsWith("#") && line.isNotEmpty) {
index++;
final tsUrl = line.startsWith("http")
? line
: "$host/${line.replaceFirst("/", "")}";
allText += "http://localhost:3000/TS_$index.ts\n";
tsList.add(TsInfo("TS_$index", tsUrl));
} else {
allText += "$line\n";
}
}
Directory(downloadDir).createSync(recursive: true);
File("$downloadDir/index.m3u8").writeAsStringSync(allText);
return tsList;
}
}

View file

@ -0,0 +1,41 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
Future<HttpServer> m3u8Server(
{required String m3u8Content, required String episodeFolderPath}) async {
final handler =
const Pipeline().addMiddleware(logRequests()).addHandler((request) {
return _handleRequest(request, m3u8Content, episodeFolderPath);
});
final server = await io.serve(handler, 'localhost', 3000);
if (kDebugMode) {
print(
'[INFO-M3U8_SERVER] Listening on running on http://${server.address.host}:${server.port}');
}
return server;
}
Response _handleRequest(
Request request, String m3u8Content, String episodeFolderPath) {
if (request.url.path == 'index.m3u8') {
return Response.ok(m3u8Content,
headers: {'Content-Type': 'application/vnd.apple.mpegurl'});
}
if (request.url.path.endsWith('.ts')) {
final tsFilePath = '$episodeFolderPath/${request.url.pathSegments.last}';
final file = File(tsFilePath);
if (file.existsSync()) {
return Response.ok(file.openRead(), headers: {
'Content-Type': 'video/MP2T',
});
} else {
return Response.notFound('File not found');
}
}
return Response.notFound('Not exist');
}

View file

@ -6,7 +6,7 @@ part of 'anilist.dart';
// RiverpodGenerator
// **************************************************************************
String _$anilistHash() => r'4d54bf86bb2f1133e77609c94b22f4ad17837f20';
String _$anilistHash() => r'd3a8852d689b13c3bde46ec05b464e7779149e58';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'kitsu.dart';
// RiverpodGenerator
// **************************************************************************
String _$kitsuHash() => r'b9e7867b0c059c8983189d8b94bc6d6a1c1bd3c5';
String _$kitsuHash() => r'6953b7520cc144f42992bbecc0d5306841c2382f';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'myanimelist.dart';
// RiverpodGenerator
// **************************************************************************
String _$myAnimeListHash() => r'90ac28c6fb5ea17c085e8ffa77eddbe48ea1948d';
String _$myAnimeListHash() => r'd69a03e6f385688047c13771528c086542e03218';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
version: "73.0.0"
_macros:
dependency: transitive
description: dart
@ -18,10 +18,10 @@ packages:
dependency: "direct overridden"
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.8.0"
analyzer_plugin:
dependency: transitive
description:
@ -215,13 +215,13 @@ packages:
source: hosted
version: "4.10.0"
collection:
dependency: transitive
dependency: "direct overridden"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
convert:
dependency: transitive
description:
@ -697,10 +697,10 @@ packages:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.1.0"
http_profile:
dependency: transitive
description:
@ -1033,7 +1033,7 @@ packages:
source: hosted
version: "3.0.1"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
@ -1377,13 +1377,13 @@ packages:
source: hosted
version: "4.0.0"
shelf:
dependency: transitive
dependency: "direct main"
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:

View file

@ -81,6 +81,8 @@ dependencies:
path: rhttp
ref: main
cupertino_http: ^1.5.1
shelf: ^1.4.2
path: ^1.9.0
dependency_overrides:
http: ^1.2.1
@ -104,6 +106,7 @@ dependency_overrides:
path: media_kit_video
ref: 50c510d018cc5286eb6730f3ea165290f19dc5f6
meta: ^1.15.0
collection: ^1.19.0
dev_dependencies:
flutter_test:

36
rust/Cargo.lock generated
View file

@ -697,9 +697,9 @@ dependencies = [
[[package]]
name = "lazy_static"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lebe"
@ -709,9 +709,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.150"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libfuzzer-sys"
@ -1096,9 +1096,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.2"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
@ -1108,9 +1108,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.3"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
@ -1154,18 +1154,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.203"
version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [
"proc-macro2",
"quote",
@ -1222,9 +1222,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.66"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
@ -1252,18 +1252,18 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",