Update la web app pour tirer partie du nouveau systeme de cache
This commit is contained in:
@@ -102,6 +102,9 @@
|
|||||||
<span v-if="track.metadata?.bitrate">
|
<span v-if="track.metadata?.bitrate">
|
||||||
{{ formatBitrate(track.metadata.bitrate) }}
|
{{ formatBitrate(track.metadata.bitrate) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="conversionLabel(track)">
|
||||||
|
{{ conversionLabel(track) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="collection" v-if="track.collection">
|
<div class="collection" v-if="track.collection">
|
||||||
{{ track.collection }}
|
{{ track.collection }}
|
||||||
@@ -153,6 +156,7 @@
|
|||||||
<p v-if="selectedTrack.metadata.sample_rate"><strong>Sample Rate:</strong> {{ formatSampleRate(selectedTrack.metadata.sample_rate) }}</p>
|
<p v-if="selectedTrack.metadata.sample_rate"><strong>Sample Rate:</strong> {{ formatSampleRate(selectedTrack.metadata.sample_rate) }}</p>
|
||||||
<p v-if="selectedTrack.metadata.bitrate"><strong>Bitrate:</strong> {{ formatBitrate(selectedTrack.metadata.bitrate) }}</p>
|
<p v-if="selectedTrack.metadata.bitrate"><strong>Bitrate:</strong> {{ formatBitrate(selectedTrack.metadata.bitrate) }}</p>
|
||||||
<p v-if="selectedTrack.metadata.channels"><strong>Channels:</strong> {{ selectedTrack.metadata.channels }}</p>
|
<p v-if="selectedTrack.metadata.channels"><strong>Channels:</strong> {{ selectedTrack.metadata.channels }}</p>
|
||||||
|
<p v-if="conversionLabel(selectedTrack)"><strong>Conversion:</strong> {{ conversionLabel(selectedTrack) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cache-section">
|
<div class="cache-section">
|
||||||
<h4>Cache Info</h4>
|
<h4>Cache Info</h4>
|
||||||
@@ -414,6 +418,37 @@ function formatDate(dateString: string) {
|
|||||||
return d.toLocaleDateString();
|
return d.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatConversion(
|
||||||
|
conversion?: { mode?: string; input_codec?: string; details?: string } | null
|
||||||
|
): string | undefined {
|
||||||
|
if (!conversion || !conversion.mode) return undefined;
|
||||||
|
const modeLower = conversion.mode.toLowerCase();
|
||||||
|
const modeLabel =
|
||||||
|
modeLower === "passthrough"
|
||||||
|
? "Passthrough"
|
||||||
|
: modeLower === "transcode"
|
||||||
|
? "Transcoded"
|
||||||
|
: conversion.mode.charAt(0).toUpperCase() + conversion.mode.slice(1);
|
||||||
|
|
||||||
|
if (conversion.input_codec) {
|
||||||
|
const codec = conversion.input_codec.toUpperCase();
|
||||||
|
if (modeLower === "passthrough") {
|
||||||
|
return `${modeLabel} (${codec})`;
|
||||||
|
}
|
||||||
|
return `${modeLabel} (${codec} → FLAC)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversion.details) {
|
||||||
|
return `${modeLabel} – ${conversion.details}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modeLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function conversionLabel(track: AudioCacheEntry | null): string | undefined {
|
||||||
|
return formatConversion(track?.metadata?.conversion ?? undefined);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshTracks();
|
refreshTracks();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface AudioCacheMetadata {
|
|||||||
sample_rate?: number;
|
sample_rate?: number;
|
||||||
bitrate?: number;
|
bitrate?: number;
|
||||||
channels?: number;
|
channels?: number;
|
||||||
|
conversion?: ConversionInfo;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,12 @@ export interface AudioCacheEntry {
|
|||||||
metadata?: AudioCacheMetadata | null;
|
metadata?: AudioCacheMetadata | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConversionInfo {
|
||||||
|
mode: string;
|
||||||
|
input_codec?: string;
|
||||||
|
details?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddTrackRequest {
|
export interface AddTrackRequest {
|
||||||
url: string;
|
url: string;
|
||||||
collection?: string;
|
collection?: string;
|
||||||
@@ -43,9 +50,13 @@ export interface AddTrackResponse {
|
|||||||
|
|
||||||
export interface DownloadStatus {
|
export interface DownloadStatus {
|
||||||
pk: string;
|
pk: string;
|
||||||
status: "pending" | "downloading" | "completed" | "failed";
|
in_progress: boolean;
|
||||||
progress?: number;
|
finished: boolean;
|
||||||
|
current_size?: number;
|
||||||
|
transformed_size?: number;
|
||||||
|
expected_size?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
conversion?: ConversionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiError {
|
export interface ApiError {
|
||||||
|
|||||||
@@ -103,7 +103,16 @@ pub async fn add_with_metadata_extraction(
|
|||||||
let flac_bytes = tokio::fs::read(&file_path).await?;
|
let flac_bytes = tokio::fs::read(&file_path).await?;
|
||||||
|
|
||||||
// Extraire les métadonnées
|
// Extraire les métadonnées
|
||||||
let metadata = crate::metadata::AudioMetadata::from_bytes(&flac_bytes)?;
|
let mut metadata = crate::metadata::AudioMetadata::from_bytes(&flac_bytes)?;
|
||||||
|
|
||||||
|
if let Some(transform) = cache.transform_metadata(&pk).await {
|
||||||
|
if let Some(mode) = transform.mode {
|
||||||
|
metadata.conversion = Some(crate::metadata::AudioConversionInfo {
|
||||||
|
mode,
|
||||||
|
source_codec: transform.input_codec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
let metadata_json: Value = serde_json::to_value(&metadata)
|
let metadata_json: Value = serde_json::to_value(&metadata)
|
||||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||||
// Stocker dans la DB
|
// Stocker dans la DB
|
||||||
|
|||||||
@@ -68,6 +68,23 @@ pub struct AudioMetadata {
|
|||||||
/// Bitrate moyen (kbps)
|
/// Bitrate moyen (kbps)
|
||||||
#[cfg_attr(feature = "pmoserver", schema(example = 1411))]
|
#[cfg_attr(feature = "pmoserver", schema(example = 1411))]
|
||||||
pub bitrate: Option<u32>,
|
pub bitrate: Option<u32>,
|
||||||
|
|
||||||
|
/// Informations sur la conversion appliquée lors de l'ingestion
|
||||||
|
#[cfg_attr(feature = "pmoserver", schema(example = json!({"mode":"transcode","source_codec":"mp3"})))]
|
||||||
|
pub conversion: Option<AudioConversionInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informations sur le processus de conversion
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "pmoserver", derive(ToSchema))]
|
||||||
|
pub struct AudioConversionInfo {
|
||||||
|
/// Mode de conversion (ex: "passthrough", "transcode")
|
||||||
|
#[cfg_attr(feature = "pmoserver", schema(example = "transcode"))]
|
||||||
|
pub mode: String,
|
||||||
|
|
||||||
|
/// Codec source détecté
|
||||||
|
#[cfg_attr(feature = "pmoserver", schema(example = "mp3"))]
|
||||||
|
pub source_codec: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioMetadata {
|
impl AudioMetadata {
|
||||||
@@ -94,6 +111,7 @@ impl AudioMetadata {
|
|||||||
sample_rate: properties.sample_rate(),
|
sample_rate: properties.sample_rate(),
|
||||||
channels: properties.channels(),
|
channels: properties.channels(),
|
||||||
bitrate: properties.audio_bitrate(),
|
bitrate: properties.audio_bitrate(),
|
||||||
|
conversion: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(tag) = tag {
|
if let Some(tag) = tag {
|
||||||
@@ -232,6 +250,7 @@ impl Default for AudioMetadata {
|
|||||||
sample_rate: None,
|
sample_rate: None,
|
||||||
channels: None,
|
channels: None,
|
||||||
bitrate: None,
|
bitrate: None,
|
||||||
|
conversion: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,6 +275,7 @@ mod tests {
|
|||||||
sample_rate: Some(44100),
|
sample_rate: Some(44100),
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
bitrate: Some(1411),
|
bitrate: Some(1411),
|
||||||
|
conversion: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -280,6 +300,7 @@ mod tests {
|
|||||||
sample_rate: None,
|
sample_rate: None,
|
||||||
channels: None,
|
channels: None,
|
||||||
bitrate: None,
|
bitrate: None,
|
||||||
|
conversion: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(metadata.collection_key(), None);
|
assert_eq!(metadata.collection_key(), None);
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
//! while native FLAC input is forwarded byte-for-byte without re-encoding.
|
//! while native FLAC input is forwarded byte-for-byte without re-encoding.
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use pmocache::download::TransformMetadata;
|
||||||
use pmocache::StreamTransformer;
|
use pmocache::StreamTransformer;
|
||||||
use pmoflac::{transcode_to_flac_stream, AudioCodec, TranscodeOptions};
|
use pmoflac::{transcode_to_flac_stream, AudioCodec, TranscodeOptions};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
/// Creates the transformer consumed by the audio cache.
|
/// Creates the transformer consumed by the audio cache.
|
||||||
pub fn create_streaming_flac_transformer() -> StreamTransformer {
|
pub fn create_streaming_flac_transformer() -> StreamTransformer {
|
||||||
Box::new(|input, mut file, progress| {
|
Box::new(|input, mut file, context| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let byte_stream = input.into_byte_stream();
|
let byte_stream = input.into_byte_stream();
|
||||||
let reader = StreamToAsyncRead::new(byte_stream);
|
let reader = StreamToAsyncRead::new(byte_stream);
|
||||||
@@ -21,7 +22,23 @@ pub fn create_streaming_flac_transformer() -> StreamTransformer {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Audio transcode error: {}", e))?;
|
.map_err(|e| format!("Audio transcode error: {}", e))?;
|
||||||
|
|
||||||
log_stream_info(transcode.input_codec(), transcode.input_stream_info());
|
let codec = transcode.input_codec();
|
||||||
|
let info = transcode.input_stream_info().clone();
|
||||||
|
log_stream_info(codec, &info);
|
||||||
|
|
||||||
|
let mode = if transcode.is_passthrough() {
|
||||||
|
"passthrough"
|
||||||
|
} else {
|
||||||
|
"transcode"
|
||||||
|
};
|
||||||
|
|
||||||
|
context
|
||||||
|
.set_metadata(TransformMetadata {
|
||||||
|
mode: Some(mode.to_string()),
|
||||||
|
input_codec: Some(codec_to_string(codec)),
|
||||||
|
details: None,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
let mut flac_stream = transcode.into_stream();
|
let mut flac_stream = transcode.into_stream();
|
||||||
let mut buffer = vec![0u8; 64 * 1024];
|
let mut buffer = vec![0u8; 64 * 1024];
|
||||||
@@ -42,7 +59,7 @@ pub fn create_streaming_flac_transformer() -> StreamTransformer {
|
|||||||
.map_err(|e| format!("Failed to write FLAC file: {}", e))?;
|
.map_err(|e| format!("Failed to write FLAC file: {}", e))?;
|
||||||
|
|
||||||
total_written += read as u64;
|
total_written += read as u64;
|
||||||
progress(total_written);
|
context.report_progress(total_written);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.flush()
|
file.flush()
|
||||||
@@ -70,6 +87,18 @@ fn log_stream_info(codec: AudioCodec, info: &pmoflac::StreamInfo) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn codec_to_string(codec: AudioCodec) -> String {
|
||||||
|
match codec {
|
||||||
|
AudioCodec::Flac => "flac",
|
||||||
|
AudioCodec::Mp3 => "mp3",
|
||||||
|
AudioCodec::OggVorbis => "ogg_vorbis",
|
||||||
|
AudioCodec::OggOpus => "ogg_opus",
|
||||||
|
AudioCodec::Wav => "wav",
|
||||||
|
AudioCodec::Aiff => "aiff",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Adapter exposing a byte stream as `AsyncRead`.
|
/// Adapter exposing a byte stream as `AsyncRead`.
|
||||||
struct StreamToAsyncRead {
|
struct StreamToAsyncRead {
|
||||||
stream: futures_util::stream::BoxStream<'static, Result<Bytes, String>>,
|
stream: futures_util::stream::BoxStream<'static, Result<Bytes, String>>,
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ fn main() {
|
|||||||
println!(" dl.wait_until_finished().await?;\n");
|
println!(" dl.wait_until_finished().await?;\n");
|
||||||
|
|
||||||
println!("2. Téléchargement avec transformation:");
|
println!("2. Téléchargement avec transformation:");
|
||||||
println!(
|
println!(" let transformer: StreamTransformer = Box::new(|response, mut file, ctx| {{");
|
||||||
" let transformer: StreamTransformer = Box::new(|response, mut file, update_progress| {{"
|
|
||||||
);
|
|
||||||
println!(" Box::pin(async move {{");
|
println!(" Box::pin(async move {{");
|
||||||
println!(" let mut stream = response.bytes_stream();");
|
println!(" let mut stream = response.bytes_stream();");
|
||||||
println!(" let mut total = 0u64;");
|
println!(" let mut total = 0u64;");
|
||||||
@@ -26,7 +24,7 @@ fn main() {
|
|||||||
println!();
|
println!();
|
||||||
println!(" file.write_all(&transformed).await.map_err(|e| e.to_string())?;");
|
println!(" file.write_all(&transformed).await.map_err(|e| e.to_string())?;");
|
||||||
println!(" total += transformed.len() as u64;");
|
println!(" total += transformed.len() as u64;");
|
||||||
println!(" update_progress(total);");
|
println!(" ctx.report_progress(total);");
|
||||||
println!(" }}");
|
println!(" }}");
|
||||||
println!();
|
println!();
|
||||||
println!(" file.flush().await.map_err(|e| e.to_string())?;");
|
println!(" file.flush().await.map_err(|e| e.to_string())?;");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ fn create_gzip_transformer() -> StreamTransformer {
|
|||||||
unimplemented!("Cette fonction nécessite la dépendance async-compression")
|
unimplemented!("Cette fonction nécessite la dépendance async-compression")
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Box::new(|response, mut file, update_progress| {
|
Box::new(|response, mut file, context| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
use async_compression::tokio::write::GzipEncoder;
|
use async_compression::tokio::write::GzipEncoder;
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ fn create_gzip_transformer() -> StreamTransformer {
|
|||||||
.map_err(|e| format!("Failed to write compressed data: {}", e))?;
|
.map_err(|e| format!("Failed to write compressed data: {}", e))?;
|
||||||
|
|
||||||
total_written += chunk.len() as u64;
|
total_written += chunk.len() as u64;
|
||||||
update_progress(total_written);
|
context.report_progress(total_written);
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder
|
encoder
|
||||||
@@ -52,7 +52,7 @@ fn create_gzip_transformer() -> StreamTransformer {
|
|||||||
|
|
||||||
/// Exemple de transformer qui convertit les données en majuscules (exemple simple)
|
/// Exemple de transformer qui convertit les données en majuscules (exemple simple)
|
||||||
fn create_uppercase_transformer() -> StreamTransformer {
|
fn create_uppercase_transformer() -> StreamTransformer {
|
||||||
Box::new(|input, mut file, update_progress| {
|
Box::new(|input, mut file, context| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let mut stream = input.into_byte_stream();
|
let mut stream = input.into_byte_stream();
|
||||||
let mut total_written = 0u64;
|
let mut total_written = 0u64;
|
||||||
@@ -77,7 +77,7 @@ fn create_uppercase_transformer() -> StreamTransformer {
|
|||||||
.map_err(|e| format!("Failed to write: {}", e))?;
|
.map_err(|e| format!("Failed to write: {}", e))?;
|
||||||
|
|
||||||
total_written += transformed.len() as u64;
|
total_written += transformed.len() as u64;
|
||||||
update_progress(total_written);
|
context.report_progress(total_written);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.flush()
|
file.flush()
|
||||||
@@ -91,7 +91,7 @@ fn create_uppercase_transformer() -> StreamTransformer {
|
|||||||
|
|
||||||
/// Exemple de transformer qui saute les N premiers bytes (utile pour enlever des headers)
|
/// Exemple de transformer qui saute les N premiers bytes (utile pour enlever des headers)
|
||||||
fn create_skip_header_transformer(skip_bytes: usize) -> StreamTransformer {
|
fn create_skip_header_transformer(skip_bytes: usize) -> StreamTransformer {
|
||||||
Box::new(move |input, mut file, update_progress| {
|
Box::new(move |input, mut file, context| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let mut stream = input.into_byte_stream();
|
let mut stream = input.into_byte_stream();
|
||||||
let mut skipped = 0usize;
|
let mut skipped = 0usize;
|
||||||
@@ -118,7 +118,7 @@ fn create_skip_header_transformer(skip_bytes: usize) -> StreamTransformer {
|
|||||||
.map_err(|e| format!("Failed to write: {}", e))?;
|
.map_err(|e| format!("Failed to write: {}", e))?;
|
||||||
|
|
||||||
total_written += to_write.len() as u64;
|
total_written += to_write.len() as u64;
|
||||||
update_progress(total_written);
|
context.report_progress(total_written);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.flush()
|
file.flush()
|
||||||
@@ -132,7 +132,7 @@ fn create_skip_header_transformer(skip_bytes: usize) -> StreamTransformer {
|
|||||||
|
|
||||||
/// Exemple de transformer qui compte les lignes et ajoute des numéros
|
/// Exemple de transformer qui compte les lignes et ajoute des numéros
|
||||||
fn create_line_number_transformer() -> StreamTransformer {
|
fn create_line_number_transformer() -> StreamTransformer {
|
||||||
Box::new(|input, mut file, update_progress| {
|
Box::new(|input, mut file, context| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let mut stream = input.into_byte_stream();
|
let mut stream = input.into_byte_stream();
|
||||||
let mut line_number = 1u32;
|
let mut line_number = 1u32;
|
||||||
@@ -162,7 +162,7 @@ fn create_line_number_transformer() -> StreamTransformer {
|
|||||||
.map_err(|e| format!("Failed to write: {}", e))?;
|
.map_err(|e| format!("Failed to write: {}", e))?;
|
||||||
|
|
||||||
total_written += numbered_line.len() as u64 + line.len() as u64 + 1;
|
total_written += numbered_line.len() as u64 + line.len() as u64 + 1;
|
||||||
update_progress(total_written);
|
context.report_progress(total_written);
|
||||||
|
|
||||||
line_number += 1;
|
line_number += 1;
|
||||||
buffer.drain(..=newline_pos);
|
buffer.drain(..=newline_pos);
|
||||||
@@ -181,7 +181,7 @@ fn create_line_number_transformer() -> StreamTransformer {
|
|||||||
.map_err(|e| format!("Failed to write: {}", e))?;
|
.map_err(|e| format!("Failed to write: {}", e))?;
|
||||||
|
|
||||||
total_written += numbered_line.len() as u64 + buffer.len() as u64;
|
total_written += numbered_line.len() as u64 + buffer.len() as u64;
|
||||||
update_progress(total_written);
|
context.report_progress(total_written);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.flush()
|
file.flush()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use axum::{
|
|||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
@@ -39,6 +40,21 @@ pub struct DownloadStatus {
|
|||||||
pub finished: bool,
|
pub finished: bool,
|
||||||
/// Erreur éventuelle
|
/// Erreur éventuelle
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
|
/// Informations sur la conversion
|
||||||
|
pub conversion: Option<ConversionStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informations sur la conversion en cours ou réalisée
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(ToSchema))]
|
||||||
|
pub struct ConversionStatus {
|
||||||
|
/// Mode de conversion (ex: "passthrough", "transcode")
|
||||||
|
#[cfg_attr(feature = "openapi", schema(example = "passthrough"))]
|
||||||
|
pub mode: String,
|
||||||
|
/// Codec source détecté (si disponible)
|
||||||
|
pub input_codec: Option<String>,
|
||||||
|
/// Informations complémentaires lisibles (optionnel)
|
||||||
|
pub details: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requête pour ajouter un item au cache
|
/// Requête pour ajouter un item au cache
|
||||||
@@ -134,30 +150,74 @@ pub async fn get_download_status<C: CacheConfig>(
|
|||||||
State(cache): State<Arc<Cache<C>>>,
|
State(cache): State<Arc<Cache<C>>>,
|
||||||
Path(pk): Path<String>,
|
Path(pk): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Vérifier que l'item existe dans la DB
|
let entry = match cache.db.get(&pk, false) {
|
||||||
if cache.db.get(&pk, false).is_err() {
|
Ok(entry) => entry,
|
||||||
return (
|
Err(_) => {
|
||||||
StatusCode::NOT_FOUND,
|
return (
|
||||||
Json(ErrorResponse {
|
StatusCode::NOT_FOUND,
|
||||||
error: "NOT_FOUND".to_string(),
|
Json(ErrorResponse {
|
||||||
message: format!("Item with pk '{}' not found in cache", pk),
|
error: "NOT_FOUND".to_string(),
|
||||||
}),
|
message: format!("Item with pk '{}' not found in cache", pk),
|
||||||
)
|
}),
|
||||||
.into_response();
|
)
|
||||||
}
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let in_progress = cache.get_download(&pk).await.is_some();
|
let download = cache.get_download(&pk).await;
|
||||||
let current_size = cache.current_size(&pk).await;
|
let file_path = cache.get_file_path(&pk);
|
||||||
let transformed_size = cache.transformed_size(&pk).await;
|
let file_size = if file_path.exists() {
|
||||||
let expected_size = cache.expected_size(&pk).await;
|
std::fs::metadata(&file_path).ok().map(|m| m.len())
|
||||||
let finished = cache.is_finished(&pk).await;
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let error = if let Some(download) = cache.get_download(&pk).await {
|
let in_progress = download.is_some();
|
||||||
|
let current_size = if let Some(download) = download.as_ref() {
|
||||||
|
Some(download.current_size().await)
|
||||||
|
} else {
|
||||||
|
file_size
|
||||||
|
};
|
||||||
|
|
||||||
|
let transformed_size = if let Some(download) = download.as_ref() {
|
||||||
|
Some(download.transformed_size().await)
|
||||||
|
} else {
|
||||||
|
file_size
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_size = if let Some(download) = download.as_ref() {
|
||||||
|
download.expected_size().await
|
||||||
|
} else {
|
||||||
|
file_size
|
||||||
|
};
|
||||||
|
|
||||||
|
let finished = if let Some(download) = download.as_ref() {
|
||||||
|
download.finished().await
|
||||||
|
} else {
|
||||||
|
file_path.exists()
|
||||||
|
};
|
||||||
|
|
||||||
|
let error = if let Some(download) = download.as_ref() {
|
||||||
download.error().await
|
download.error().await
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut conversion = if let Some(download) = download.as_ref() {
|
||||||
|
download
|
||||||
|
.transform_metadata()
|
||||||
|
.await
|
||||||
|
.map(ConversionStatus::from)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if conversion.is_none() {
|
||||||
|
if let Some(meta) = entry.metadata.as_ref() {
|
||||||
|
conversion = conversion_from_json(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let status = DownloadStatus {
|
let status = DownloadStatus {
|
||||||
pk,
|
pk,
|
||||||
in_progress,
|
in_progress,
|
||||||
@@ -166,11 +226,28 @@ pub async fn get_download_status<C: CacheConfig>(
|
|||||||
expected_size,
|
expected_size,
|
||||||
finished,
|
finished,
|
||||||
error,
|
error,
|
||||||
|
conversion,
|
||||||
};
|
};
|
||||||
|
|
||||||
(StatusCode::OK, Json(status)).into_response()
|
(StatusCode::OK, Json(status)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<crate::download::TransformMetadata> for ConversionStatus {
|
||||||
|
fn from(value: crate::download::TransformMetadata) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: value.mode.unwrap_or_else(|| "unknown".to_string()),
|
||||||
|
input_codec: value.input_codec,
|
||||||
|
details: value.details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conversion_from_json(value: &Value) -> Option<ConversionStatus> {
|
||||||
|
value
|
||||||
|
.get("conversion")
|
||||||
|
.and_then(|conv| serde_json::from_value(conv.clone()).ok())
|
||||||
|
}
|
||||||
|
|
||||||
/// Ajoute un item au cache depuis une URL
|
/// Ajoute un item au cache depuis une URL
|
||||||
///
|
///
|
||||||
/// Télécharge l'item depuis l'URL fournie et l'ajoute au cache.
|
/// Télécharge l'item depuis l'URL fournie et l'ajoute au cache.
|
||||||
|
|||||||
@@ -94,9 +94,10 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
///
|
///
|
||||||
/// let transformer_factory = Arc::new(|| {
|
/// let transformer_factory = Arc::new(|| {
|
||||||
/// // Créer un transformer qui convertit les données
|
/// // Créer un transformer qui convertit les données
|
||||||
/// Box::new(|input, file, progress| {
|
/// Box::new(|input, file, ctx| {
|
||||||
/// Box::pin(async move {
|
/// Box::pin(async move {
|
||||||
/// // Transformation personnalisée
|
/// // Transformation personnalisée
|
||||||
|
/// ctx.report_progress(0);
|
||||||
/// Ok(())
|
/// Ok(())
|
||||||
/// })
|
/// })
|
||||||
/// }) as StreamTransformer
|
/// }) as StreamTransformer
|
||||||
@@ -610,6 +611,15 @@ impl<C: CacheConfig> Cache<C> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retourne les métadonnées de transformation (si disponibles)
|
||||||
|
pub async fn transform_metadata(&self, pk: &str) -> Option<crate::download::TransformMetadata> {
|
||||||
|
if let Some(download) = self.get_download(pk).await {
|
||||||
|
download.transform_metadata().await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Indique si le téléchargement est terminé
|
/// Indique si le téléchargement est terminé
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -15,18 +15,20 @@ use tokio_util::io::ReaderStream;
|
|||||||
/// La fonction reçoit :
|
/// La fonction reçoit :
|
||||||
/// - Un `CacheInput` abstrait (HTTP ou lecteur en streaming)
|
/// - Un `CacheInput` abstrait (HTTP ou lecteur en streaming)
|
||||||
/// - Un writer pour écrire les données transformées
|
/// - Un writer pour écrire les données transformées
|
||||||
/// - Un callback pour mettre à jour la progression
|
/// - Un contexte fournissant des utilitaires (progression, métadonnées)
|
||||||
///
|
///
|
||||||
/// Elle retourne un `Future` qui se résout en `Result`.
|
/// Elle retourne un `Future` qui se résout en `Result`.
|
||||||
pub type StreamTransformer = Box<
|
pub type StreamTransformer = Box<
|
||||||
dyn FnOnce(
|
dyn FnOnce(
|
||||||
CacheInput,
|
CacheInput,
|
||||||
tokio::fs::File,
|
tokio::fs::File,
|
||||||
Arc<dyn Fn(u64) + Send + Sync>,
|
TransformContextHandle,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>>
|
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>>
|
||||||
+ Send,
|
+ Send,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
pub type TransformContextHandle = Arc<TransformContext>;
|
||||||
|
|
||||||
type ByteStream = Pin<Box<dyn Stream<Item = Result<Bytes, String>> + Send>>;
|
type ByteStream = Pin<Box<dyn Stream<Item = Result<Bytes, String>> + Send>>;
|
||||||
|
|
||||||
/// Source générique (HTTP ou lecteur) exposée aux transformers.
|
/// Source générique (HTTP ou lecteur) exposée aux transformers.
|
||||||
@@ -180,6 +182,7 @@ struct DownloadState {
|
|||||||
finished: bool,
|
finished: bool,
|
||||||
read_position: u64,
|
read_position: u64,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
|
transform_metadata: Option<TransformMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Objet représentant un téléchargement en cours
|
/// Objet représentant un téléchargement en cours
|
||||||
@@ -200,6 +203,7 @@ impl Download {
|
|||||||
finished: false,
|
finished: false,
|
||||||
read_position: 0,
|
read_position: 0,
|
||||||
error: None,
|
error: None,
|
||||||
|
transform_metadata: None,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -274,6 +278,45 @@ impl Download {
|
|||||||
let state = self.state.read().await;
|
let state = self.state.read().await;
|
||||||
state.error.clone()
|
state.error.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn transform_metadata(&self) -> Option<TransformMetadata> {
|
||||||
|
let state = self.state.read().await;
|
||||||
|
state.transform_metadata.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TransformMetadata {
|
||||||
|
pub mode: Option<String>,
|
||||||
|
pub input_codec: Option<String>,
|
||||||
|
pub details: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TransformContext {
|
||||||
|
state: Arc<RwLock<DownloadState>>,
|
||||||
|
progress_cb: Arc<dyn Fn(u64) + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransformContext {
|
||||||
|
fn new(state: Arc<RwLock<DownloadState>>, progress_cb: Arc<dyn Fn(u64) + Send + Sync>) -> Self {
|
||||||
|
Self { state, progress_cb }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports progress (in bytes) to the download state.
|
||||||
|
pub fn report_progress(&self, bytes: u64) {
|
||||||
|
(self.progress_cb)(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the underlying progress callback (useful for piping into other APIs).
|
||||||
|
pub fn progress_callback(&self) -> Arc<dyn Fn(u64) + Send + Sync> {
|
||||||
|
Arc::clone(&self.progress_cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores metadata describing the transformation that occurred.
|
||||||
|
pub async fn set_metadata(&self, metadata: TransformMetadata) {
|
||||||
|
let mut state = self.state.write().await;
|
||||||
|
state.transform_metadata = Some(metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lance le téléchargement d'une URL dans un fichier.
|
/// Lance le téléchargement d'une URL dans un fichier.
|
||||||
@@ -402,7 +445,9 @@ async fn process_input(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
match transformer(input, file, Arc::clone(&progress_callback)).await {
|
let context = Arc::new(TransformContext::new(Arc::clone(&state), progress_callback));
|
||||||
|
|
||||||
|
match transformer(input, file, Arc::clone(&context)).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let mut s = state.write().await;
|
let mut s = state.write().await;
|
||||||
if s.current_size == 0 {
|
if s.current_size == 0 {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ pub use cache_trait::{pk_from_content_header, FileCache};
|
|||||||
pub use db::{CacheEntry, DB};
|
pub use db::{CacheEntry, DB};
|
||||||
pub use download::{
|
pub use download::{
|
||||||
download, download_with_transformer, ingest_with_transformer, peek_header, peek_reader_header,
|
download, download_with_transformer, ingest_with_transformer, peek_header, peek_reader_header,
|
||||||
Download, StreamTransformer,
|
Download, StreamTransformer, TransformContextHandle, TransformMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub type Cache = pmocache::Cache<CoversConfig>;
|
|||||||
///
|
///
|
||||||
/// Convertit automatiquement toute image téléchargée en format WebP
|
/// Convertit automatiquement toute image téléchargée en format WebP
|
||||||
fn create_webp_transformer() -> StreamTransformer {
|
fn create_webp_transformer() -> StreamTransformer {
|
||||||
Box::new(|mut input, mut file, progress| {
|
Box::new(|mut input, mut file, context| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Télécharger tout en mémoire
|
// Télécharger tout en mémoire
|
||||||
let bytes = input.bytes().await?;
|
let bytes = input.bytes().await?;
|
||||||
@@ -48,7 +48,7 @@ fn create_webp_transformer() -> StreamTransformer {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
file.flush().await.map_err(|e| e.to_string())?;
|
file.flush().await.map_err(|e| e.to_string())?;
|
||||||
progress(webp_data.len() as u64);
|
context.report_progress(webp_data.len() as u64);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ impl QobuzSource {
|
|||||||
sample_rate: track.sample_rate,
|
sample_rate: track.sample_rate,
|
||||||
channels: track.channels,
|
channels: track.channels,
|
||||||
bitrate: None,
|
bitrate: None,
|
||||||
|
conversion: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Cache audio via manager
|
// 3. Cache audio via manager
|
||||||
|
|||||||
180
tools/sniff_upnp_devices
Executable file
180
tools/sniff_upnp_devices
Executable file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# UPnP Device Sniffer avec déduplication
|
||||||
|
# Découverte et analyse des devices UPnP sur le réseau
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
PURPLE='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
INTERFACE="${1:-en0}"
|
||||||
|
TIMEOUT="${2:-5}"
|
||||||
|
TEMP_FILE=$(mktemp)
|
||||||
|
SEEN_FILE=$(mktemp)
|
||||||
|
|
||||||
|
echo -e "${CYAN}"
|
||||||
|
echo "╔══════════════════════════════════════╗"
|
||||||
|
echo "║ UPnP Device Sniffer PRO ║"
|
||||||
|
echo "║ Avec déduplication des devices ║"
|
||||||
|
echo "╚══════════════════════════════════════╝"
|
||||||
|
echo -e "${NC}"
|
||||||
|
|
||||||
|
# Nettoyage à la sortie
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$TEMP_FILE" "$SEEN_FILE"
|
||||||
|
echo -e "${YELLOW}🧹 Fichiers temporaires nettoyés.${NC}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Fonction pour extraire les codecs audio
|
||||||
|
extract_audio_codecs() {
|
||||||
|
grep -i "protocolInfo" | grep -i "audio" | \
|
||||||
|
sed 's/.*\(audio.*\)/\1/' | sort -u | head -8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour formater l'URL
|
||||||
|
format_url() {
|
||||||
|
echo "$1" | sed 's/.*http/http/' | sed 's/\/$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour normaliser le nom du device
|
||||||
|
normalize_name() {
|
||||||
|
echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]//g'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour vérifier si un device a déjà été vu
|
||||||
|
is_device_seen() {
|
||||||
|
local key="$1"
|
||||||
|
grep -q "^$key$" "$SEEN_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour marquer un device comme vu
|
||||||
|
mark_device_seen() {
|
||||||
|
local key="$1"
|
||||||
|
echo "$key" >> "$SEEN_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e "${YELLOW}🔍 Recherche des devices UPnP sur $INTERFACE (${TIMEOUT}s)...${NC}"
|
||||||
|
echo -e "${YELLOW}📡 Filtrage des doublons activé...${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Phase 1: Collecte des devices avec déduplication
|
||||||
|
echo -e "${BLUE}📦 Phase 1: Collecte des devices...${NC}"
|
||||||
|
|
||||||
|
DEVICE_COUNT=0
|
||||||
|
|
||||||
|
gssdp-discover --timeout $TIMEOUT -i $INTERFACE 2>/dev/null | \
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ $line == *"Location:"* ]]; then
|
||||||
|
url=$(format_url "$(echo "$line" | awk '{print $2}')")
|
||||||
|
|
||||||
|
# Récupération des infos de base pour la déduplication
|
||||||
|
if curl_output=$(curl -s --connect-timeout 2 "$url" 2>/dev/null); then
|
||||||
|
friendly_name=$(echo "$curl_output" | grep -i "friendlyName" | head -1 | sed 's/.*<friendlyName>\(.*\)<\/friendlyName>.*/\1/')
|
||||||
|
model_name=$(echo "$curl_output" | grep -i "modelName" | head -1 | sed 's/.*<modelName>\(.*\)<\/modelName>.*/\1/')
|
||||||
|
|
||||||
|
# Création d'une clé unique pour la déduplication
|
||||||
|
device_key=$(normalize_name "${friendly_name}-${model_name}")
|
||||||
|
|
||||||
|
# Vérifier si on a déjà vu ce device
|
||||||
|
if ! is_device_seen "$device_key"; then
|
||||||
|
mark_device_seen "$device_key"
|
||||||
|
((DEVICE_COUNT++))
|
||||||
|
|
||||||
|
# Stocker les données pour l'affichage
|
||||||
|
echo "===DEVICE_START===" >> "$TEMP_FILE"
|
||||||
|
echo "URL:$url" >> "$TEMP_FILE"
|
||||||
|
echo "FRIENDLY_NAME:$friendly_name" >> "$TEMP_FILE"
|
||||||
|
echo "MODEL:$model_name" >> "$TEMP_FILE"
|
||||||
|
echo "MANUFACTURER:$(echo "$curl_output" | grep -i "manufacturer" | head -1 | sed 's/.*<manufacturer>\(.*\)<\/manufacturer>.*/\1/')" >> "$TEMP_FILE"
|
||||||
|
echo "CURL_OUTPUT:$curl_output" >> "$TEMP_FILE"
|
||||||
|
echo "===DEVICE_END===" >> "$TEMP_FILE"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW} ⚡ Doublon ignoré: $friendly_name${NC}" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Phase 2: Affichage organisé des devices uniques
|
||||||
|
echo -e "${BLUE}📊 Phase 2: Analyse des devices uniques...${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
FINAL_COUNT=0
|
||||||
|
|
||||||
|
# Lire le fichier temporaire et afficher les résultats
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case $line in
|
||||||
|
"===DEVICE_START===")
|
||||||
|
unset url friendly_name model_name manufacturer curl_output
|
||||||
|
;;
|
||||||
|
"===DEVICE_END===")
|
||||||
|
if [[ -n "$url" ]]; then
|
||||||
|
((FINAL_COUNT++))
|
||||||
|
echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ Device #$FINAL_COUNT${NC}"
|
||||||
|
echo -e "${GREEN}║ URL: $url${NC}"
|
||||||
|
echo -e "${GREEN}╚══════════════════════════════════════╗${NC}"
|
||||||
|
|
||||||
|
echo -e "${BLUE}📝 Informations:${NC}"
|
||||||
|
echo -e " Nom: ${YELLOW}${friendly_name:-Non trouvé}${NC}"
|
||||||
|
echo -e " Modèle: ${YELLOW}${model_name:-Non trouvé}${NC}"
|
||||||
|
echo -e " Fabricant: ${YELLOW}${manufacturer:-Non trouvé}${NC}"
|
||||||
|
|
||||||
|
# Codecs audio supportés
|
||||||
|
echo -e "${BLUE}🎵 Codecs audio:${NC}"
|
||||||
|
audio_codecs=$(echo "$curl_output" | extract_audio_codecs)
|
||||||
|
if [ -n "$audio_codecs" ]; then
|
||||||
|
echo "$audio_codecs" | while read codec; do
|
||||||
|
echo -e " ✅ ${GREEN}$codec${NC}"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo -e " ❌ ${RED}Aucun codec audio détecté${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Services détectés
|
||||||
|
echo -e "${BLUE}🔌 Services principaux:${NC}"
|
||||||
|
services=$(echo "$curl_output" | grep -i "serviceType" | sed 's/.*<serviceType>\(.*\)<\/serviceType>.*/\1/' | head -3)
|
||||||
|
if [ -n "$services" ]; then
|
||||||
|
echo "$services" | while read service; do
|
||||||
|
echo -e " 🔧 ${PURPLE}$service${NC}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
URL:*)
|
||||||
|
url="${line#URL:}"
|
||||||
|
;;
|
||||||
|
FRIENDLY_NAME:*)
|
||||||
|
friendly_name="${line#FRIENDLY_NAME:}"
|
||||||
|
;;
|
||||||
|
MODEL:*)
|
||||||
|
model_name="${line#MODEL:}"
|
||||||
|
;;
|
||||||
|
MANUFACTURER:*)
|
||||||
|
manufacturer="${line#MANUFACTURER:}"
|
||||||
|
;;
|
||||||
|
CURL_OUTPUT:*)
|
||||||
|
curl_output="${line#CURL_OUTPUT:}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < "$TEMP_FILE"
|
||||||
|
|
||||||
|
# Résumé final
|
||||||
|
echo -e "${CYAN}"
|
||||||
|
echo "╔══════════════════════════════════════╗"
|
||||||
|
echo "║ SCAN TERMINÉ ║"
|
||||||
|
echo "╠══════════════════════════════════════╣"
|
||||||
|
echo "║ Devices uniques: $FINAL_COUNT ║"
|
||||||
|
echo "╚══════════════════════════════════════╝"
|
||||||
|
echo -e "${NC}"
|
||||||
Reference in New Issue
Block a user