Skip to content

Commit

Permalink
Expand file type detection
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Dec 16, 2024
1 parent 41afb95 commit 203110d
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 39 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
## Unreleased

* Added:
* When the app can't detect a file's type,
it will try checking the system's shared MIME database (if available on Linux/Mac),
and then further fall back to guessing based on the file extension.
* Partial translations into Polish and French.
(Thanks to contributors on the [Crowdin project](https://crowdin.com/project/madamiru))
* Changed:
* The app previously used a known set of supported video formats and ignored other video files.
However, since the exact set depends on which GStreamer plugins you've installed,
the app will now simply try loading any video file.
* Fixed:
* The `crop` content fit now works correctly for videos.
Previously, it acted the same as `stretch`.
Expand Down
47 changes: 47 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ infer = "0.16.0"
intl-memoizer = "0.5.2"
itertools = "0.13.0"
log = "0.4.22"
mime_guess = "2.0.5"
opener = "0.7.2"
rand = "0.8.5"
realia = "0.2.0"
Expand All @@ -45,6 +46,7 @@ serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
serde_yaml = "0.8.25"
tokio = { version = "1.40.0", features = ["macros", "time"] }
tree_magic_mini = "3.1.6"
typed-path = "0.9.2"
unic-langid = "0.9.5"
url = "2.5.3"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ that can automatically shuffle multiple videos, images, and songs at once in a g
## Features
* Customizable layout with multiple groups of dynamically selected media
* Video formats: AVI, M4V, MKV, MOV, MP4, WebM
<!-- , plus any others supported by [GStreamer](https://gstreamer.freedesktop.org) -->
* Image formats: BMP, GIF, ICO, JPEG, PNG, TIFF, SVG, WebP
* Audio formats: FLAC, M4A, MP3, WAV
* Subtitles are supported within MKV (but not as separate files)
Expand Down
125 changes: 86 additions & 39 deletions src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,31 @@ impl ToString for SourceKind {
}
}

#[derive(Debug)]
enum Mime {
/// From the `infer` crate.
/// Based on magic bytes without system dependencies, but not exhaustive.
Pure(&'static str),
/// From the `tree_magic_mini` crate.
/// Uses the system's shared database on Linux and Mac,
/// but not viable for Windows without bundling GPL data.
#[allow(unused)]
Database(&'static str),
/// From the `mime_guess` crate.
/// Guesses based on the file extension.
Extension(mime_guess::Mime),
}

impl Mime {
fn essence(&self) -> &str {
match self {
Self::Pure(raw) => raw,
Self::Database(raw) => raw,
Self::Extension(mime) => mime.essence_str(),
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Media {
Image {
Expand Down Expand Up @@ -185,50 +210,72 @@ impl Media {
}
};

match infer::get_from_path(inferrable) {
Ok(Some(info)) => {
log::debug!("Inferred file type '{}': {path:?}", info.mime_type());
#[allow(clippy::unnecessary_lazy_evaluations)]
let mime = infer::get_from_path(&inferrable)
.map_err(|e| {
log::error!("Error inferring file type: {path:?} | {e:?}");
e
})
.ok()
.flatten()
.map(|x| Mime::Pure(x.mime_type()))
.or_else(|| {
#[cfg(target_os = "windows")]
{
None
}
#[cfg(not(target_os = "windows"))]
{
tree_magic_mini::from_filepath(&inferrable).map(Mime::Database)
}
})
.or_else(|| mime_guess::from_path(&inferrable).first().map(Mime::Extension));

let extension = path.file_extension().map(|x| x.to_lowercase());
log::debug!("Inferred file type '{:?}': {path:?}", mime);

match info.mime_type() {
#[cfg(feature = "video")]
"video/mp4" | "video/quicktime" | "video/webm" | "video/x-m4v" | "video/x-matroska"
| "video/x-msvideo" => Some(Self::Video {
path: path.normalized(),
}),
#[cfg(feature = "audio")]
"audio/mpeg" | "audio/m4a" | "audio/x-flac" | "audio/x-wav" => Some(Self::Audio {
path: path.normalized(),
}),
"image/bmp"
| "image/jpeg"
| "image/png"
| "image/tiff"
| "image/vnd.microsoft.icon"
| "image/webp" => Some(Self::Image {
path: path.normalized(),
}),
"image/gif" => Some(Self::Gif {
mime.and_then(|mime| {
let mime = mime.essence();

#[cfg(feature = "video")]
if mime.starts_with("video/") {
// The exact formats supported will depend on the user's GStreamer plugins,
// so just go ahead and try it. Some that work by default on Windows:
// * video/mp4
// * video/mpeg
// * video/quicktime
// * video/webm
// * video/x-m4v
// * video/x-matroska
// * video/x-msvideo
return Some(Self::Video {
path: path.normalized(),
});
}

let extension = path.file_extension().map(|x| x.to_lowercase());

match mime {
#[cfg(feature = "audio")]
"audio/mpeg" | "audio/m4a" | "audio/x-flac" | "audio/x-wav" => Some(Self::Audio {
path: path.normalized(),
}),
"image/bmp" | "image/jpeg" | "image/png" | "image/tiff" | "image/vnd.microsoft.icon" | "image/webp" => {
Some(Self::Image {
path: path.normalized(),
}),
_ => match extension.as_deref() {
Some("svg") => Some(Self::Svg {
path: path.normalized(),
}),
_ => None,
},
})
}
"image/gif" => Some(Self::Gif {
path: path.normalized(),
}),
"image/svg+xml" => Some(Self::Svg {
path: path.normalized(),
}),
"text/xml" if extension.is_some_and(|ext| ext == "svg") => Some(Self::Svg {
path: path.normalized(),
}),
_ => None,
}
Ok(None) => {
log::debug!("Did not infer any file type: {path:?}");
None
}
Err(e) => {
log::error!("Error inferring file type: {path:?} | {e:?}");
None
}
}
})
}
}

Expand Down

0 comments on commit 203110d

Please sign in to comment.