feat: add torrent-get 'primary-mime-type' to RPC. (#1464)

* feat: add torrent-get 'primary-mime-type' to RPC.

This is a cheap way for RPC clients to know what type of content is in a
torrent. This info can be used to display the torrent, e.g. by using an
icon that corresponds to the mime type.

* use size_t for content byte count

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>

* explicit boolean expressions

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>

* use uint64_t for content byte counts

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>

* avoid unnecessary logic branches

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>

* explicit cast

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>

* refactor: add an autogenerated mime-type.h header

* chore: maybe fix the win32 FTBFS

* chore: add mime-types.[ch] to xcode

* Squashed commit of the following:

commit 4c7153fa48
Author: Mike Gelfand <mikedld@users.noreply.github.com>
Date:   Tue Oct 13 03:15:19 2020 +0300

    Remove autotools-based build system (#1465)

    * Support .git files (e.g. for worktrees, submodules)
    * Fix symlinks in source tarball, switch to TXZ, adjust non-release name
    * Remove autotools stuff

Co-authored-by: Mike Gelfand <mikedld@users.noreply.github.com>
This commit is contained in:
Charles Kerr 2020-10-13 10:33:56 -05:00 committed by GitHub
parent 4c7153fa48
commit f59118d1fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1509 additions and 25 deletions

View File

@ -367,6 +367,8 @@
C1FEE5791C3223CC00D62832 /* watchdir-kqueue.c in Sources */ = {isa = PBXBuildFile; fileRef = C1FEE5741C3223CC00D62832 /* watchdir-kqueue.c */; };
C1FEE57A1C3223CC00D62832 /* watchdir.c in Sources */ = {isa = PBXBuildFile; fileRef = C1FEE5751C3223CC00D62832 /* watchdir.c */; };
C1FEE57B1C3223CC00D62832 /* watchdir.h in Headers */ = {isa = PBXBuildFile; fileRef = C1FEE5761C3223CC00D62832 /* watchdir.h */; };
CAB35C64252F6F5E00552A55 /* mime-types.h in Headers */ = {isa = PBXBuildFile; fileRef = CAB35C62252F6F5E00552A55 /* mime-types.h */; };
CAB35C65252F6F5E00552A55 /* mime-types.c in Sources */ = {isa = PBXBuildFile; fileRef = CAB35C63252F6F5E00552A55 /* mime-types.c */; };
D4AF3B2F0C41F7A500D46B6B /* list.c in Sources */ = {isa = PBXBuildFile; fileRef = D4AF3B2D0C41F7A500D46B6B /* list.c */; };
D4AF3B300C41F7A600D46B6B /* list.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF3B2E0C41F7A500D46B6B /* list.h */; };
E138A9780C04D88F00C5426C /* ProgressGradients.m in Sources */ = {isa = PBXBuildFile; fileRef = E138A9760C04D88F00C5426C /* ProgressGradients.m */; };
@ -1018,6 +1020,8 @@
C1FEE5741C3223CC00D62832 /* watchdir-kqueue.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = "watchdir-kqueue.c"; sourceTree = "<group>"; };
C1FEE5751C3223CC00D62832 /* watchdir.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = watchdir.c; sourceTree = "<group>"; };
C1FEE5761C3223CC00D62832 /* watchdir.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = watchdir.h; sourceTree = "<group>"; };
CAB35C62252F6F5E00552A55 /* mime-types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "mime-types.h"; sourceTree = "<group>"; };
CAB35C63252F6F5E00552A55 /* mime-types.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = "mime-types.c"; sourceTree = "<group>"; };
D4AF3B2D0C41F7A500D46B6B /* list.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = list.c; sourceTree = "<group>"; };
D4AF3B2E0C41F7A500D46B6B /* list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = list.h; sourceTree = "<group>"; };
E138A9750C04D88F00C5426C /* ProgressGradients.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ProgressGradients.h; sourceTree = "<group>"; };
@ -1368,6 +1372,8 @@
4D1838DC09DEC04A0047D688 /* libtransmission */ = {
isa = PBXGroup;
children = (
CAB35C63252F6F5E00552A55 /* mime-types.c */,
CAB35C62252F6F5E00552A55 /* mime-types.h */,
C1077A4A183EB29600634C22 /* error.c */,
C1077A4B183EB29600634C22 /* error.h */,
C1077A4C183EB29600634C22 /* file-posix.c */,
@ -1866,6 +1872,7 @@
A247A443114C701800547DFC /* InfoViewController.h in Headers */,
A220EC5C118C8A060022B4BE /* tr-lpd.h in Headers */,
A23547E311CD0B090046EAE6 /* cache.h in Headers */,
CAB35C64252F6F5E00552A55 /* mime-types.h in Headers */,
A284214512DA663E00FBDDBB /* tr-udp.h in Headers */,
C1077A4F183EB29600634C22 /* error.h in Headers */,
A2679295130E00A000CB7464 /* tr-utp.h in Headers */,
@ -2173,7 +2180,6 @@
29B97313FDCFA39411CA2CEA /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 0420;
ORGANIZATIONNAME = "The Transmission Project";
TargetAttributes = {
@ -2204,6 +2210,7 @@
de,
da,
"pt-PT",
pt_PT,
);
mainGroup = 29B97314FDCFA39411CA2CEA /* Transmission */;
projectDirPath = "";
@ -2414,6 +2421,7 @@
A201527E0D1C270F0081714F /* torrent-ctor.c in Sources */,
A2D22A130D65EEE700007D5F /* verify.c in Sources */,
4D4ADFC70DA1631500A68297 /* blocklist.c in Sources */,
CAB35C65252F6F5E00552A55 /* mime-types.c in Sources */,
A29DF8B90DB2544C00D04E5A /* resume.c in Sources */,
A2A4E9220DE0F7EB000CE197 /* web.c in Sources */,
A2A4EA0E0DE106EB000CE197 /* ConvertUTF.c in Sources */,

View File

@ -195,6 +195,7 @@
errorString | string | tr_stat
eta | number | tr_stat
etaIdle | number | tr_stat
file-count | number | tr_info
files | array (see below) | n/a
fileStats | array (see below) | n/a
hashString | string | tr_info
@ -223,6 +224,7 @@
pieceCount | number | tr_info
pieceSize | number | tr_info
priorities | array (see below) | n/a
primary-mime-type | string | tr_torrent
queuePosition | number | tr_stat
rateDownload (B/s) | number | tr_stat
rateUpload (B/s) | number | tr_stat
@ -809,6 +811,9 @@
| | yes | torrent-set | new arg "labels"
| | yes | torrent-get | new arg "editDate"
| | yes | torrent-get | new request arg "format"
------+---------+-----------+----------------------+-------------------------------
17 | 3.01 | yes | torrent-get | new arg "file-count"
| | yes | torrent-get | new arg "primary-mime-type"
5.1. Upcoming Breakage

View File

@ -34,6 +34,7 @@ set(PROJECT_FILES
magnet.c
makemeta.c
metainfo.c
mime-types.c
natpmp.c
net.c
peer-io.c
@ -157,6 +158,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS
list.h
magnet.h
metainfo.h
mime-types.h
natpmp_local.h
net.h
peer-common.h

1213
libtransmission/mime-types.c Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
/*
* This file Copyright (C) 2020 Mnemosyne LLC
*
* It may be used under the GNU GPL versions 2 or 3
* or any future license endorsed by Mnemosyne LLC.
*/
#pragma once
#define MIME_TYPE_SUFFIX_MAXLEN 24
#define MIME_TYPE_SUFFIX_COUNT 1201
struct mime_type_suffix
{
char const* suffix;
char const* mime_type;
};
extern struct mime_type_suffix const mime_type_suffixes[MIME_TYPE_SUFFIX_COUNT];

72
libtransmission/mime-types.js Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env node
const copyright =
`/*
* This file Copyright (C) ${new Date().getFullYear()} Mnemosyne LLC
*
* It may be used under the GNU GPL versions 2 or 3
* or any future license endorsed by Mnemosyne LLC.
*/`;
const fs = require('fs');
const https = require('https');
// https://github.com/jshttp/mime-db
// > If you're crazy enough to use this in the browser, you can just grab
// > the JSON file using jsDelivr. It is recommended to replace master with
// > a release tag as the JSON format may change in the future.
//
// This script keeps it at `master` to pick up any fixes that may have landed.
// If the format changes, we'll just update this script.
const url = 'https://cdn.jsdelivr.net/gh/jshttp/mime-db@master/db.json';
https.get(url, (res) => {
res.setEncoding('utf8');
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
try {
const suffixes = [];
const mime_types = JSON.parse(chunks.join(''));
for (const [mime_type, info] of Object.entries(mime_types)) {
for (const suffix of info?.extensions || []) {
suffixes.push([ suffix, mime_type ]);
}
}
const max_suffix_len = suffixes
.reduce((acc, [suffix]) => Math.max(acc, suffix.length), 0);
const mime_type_lines = suffixes
.map(([suffix, mime_type]) => ` { "${suffix}", "${mime_type}" }`)
.sort()
.join(',\n');
fs.writeFileSync('mime-types.c', `${copyright}
#include "mime-types.h"
struct mime_type_suffix const mime_type_suffixes[MIME_TYPE_SUFFIX_COUNT] =
{
${mime_type_lines}
};
`);
fs.writeFileSync('mime-types.h', `${copyright}
#pragma once
#define MIME_TYPE_SUFFIX_MAXLEN ${max_suffix_len}
#define MIME_TYPE_SUFFIX_COUNT ${suffixes.length}
struct mime_type_suffix
{
char const* suffix;
char const* mime_type;
};
extern struct mime_type_suffix const mime_type_suffixes[MIME_TYPE_SUFFIX_COUNT];
`);
} catch (e) {
console.error(e.message);
}
});
});

View File

@ -116,6 +116,7 @@ static struct tr_key_struct const my_static[] =
Q("etaIdle"),
Q("failure reason"),
Q("fields"),
Q("file-count"),
Q("fileStats"),
Q("filename"),
Q("files"),
@ -255,6 +256,7 @@ static struct tr_key_struct const my_static[] =
Q("port-is-open"),
Q("preallocation"),
Q("prefetch-enabled"),
Q("primary-mime-type"),
Q("priorities"),
Q("priority"),
Q("priority-high"),

View File

@ -115,6 +115,7 @@ enum
TR_KEY_etaIdle,
TR_KEY_failure_reason,
TR_KEY_fields,
TR_KEY_file_count,
TR_KEY_fileStats,
TR_KEY_filename,
TR_KEY_files,
@ -254,6 +255,7 @@ enum
TR_KEY_port_is_open,
TR_KEY_preallocation,
TR_KEY_prefetch_enabled,
TR_KEY_primary_mime_type,
TR_KEY_priorities,
TR_KEY_priority,
TR_KEY_priority_high,

View File

@ -640,6 +640,10 @@ static void initField(tr_torrent* const tor, tr_info const* const inf, tr_stat c
tr_variantInitInt(initme, st->eta);
break;
case TR_KEY_file_count:
tr_variantInitInt(initme, inf->fileCount);
break;
case TR_KEY_files:
tr_variantInitList(initme, inf->fileCount);
addFiles(tor, initme);
@ -779,6 +783,10 @@ static void initField(tr_torrent* const tor, tr_info const* const inf, tr_stat c
tr_variantInitInt(initme, inf->pieceSize);
break;
case TR_KEY_primary_mime_type:
tr_variantInitStr(initme, tr_torrentPrimaryMimeType(tor), TR_BAD_SIZE);
break;
case TR_KEY_priorities:
tr_variantInitList(initme, inf->fileCount);
for (tr_file_index_t i = 0; i < inf->fileCount; ++i)

View File

@ -3378,6 +3378,58 @@ void tr_torrentSetLocation(tr_torrent* tor, char const* location, bool move_from
tr_runInEventThread(tor->session, setLocation, data);
}
char const* tr_torrentPrimaryMimeType(tr_torrent const* tor)
{
struct count
{
uint64_t length;
char const* mime_type;
};
tr_info const* inf = &tor->info;
struct count* counts = tr_new0(struct count, inf->fileCount);
size_t num_counts = 0;
for (tr_file const* it = inf->files, * end = it + inf->fileCount; it != end; ++it)
{
char const* mime_type = tr_get_mime_type_for_filename(it->name);
size_t i;
for (i = 0; i < num_counts; ++i)
{
if (counts[i].mime_type == mime_type)
{
counts[i].length += it->length;
break;
}
}
if (i == num_counts)
{
counts[i].mime_type = mime_type;
counts[i].length = it->length;
++num_counts;
}
}
uint64_t max_len = 0;
char const* mime_type = NULL;
for (struct count const* it = counts, *end = it + num_counts; it != end; ++it)
{
if ((max_len < it->length) && (it->mime_type != NULL))
{
max_len = it->length;
mime_type = it->mime_type;
}
}
tr_free(counts);
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
// application/octet-stream is the default value for all other cases.
// An unknown file type should use this type.
return mime_type != NULL ? mime_type : "application/octet-stream";
}
/***
****
***/

View File

@ -96,6 +96,10 @@ void tr_torrentSetDateActive(tr_torrent* torrent, time_t activityDate);
void tr_torrentSetDateDone(tr_torrent* torrent, time_t doneDate);
/** Return the mime-type (e.g. "audio/x-flac") that matches more of the
torrent's content than any other mime-type. */
char const* tr_torrentPrimaryMimeType(tr_torrent const* tor);
typedef enum
{
TR_VERIFY_NONE,

View File

@ -49,6 +49,7 @@
#include "ConvertUTF.h"
#include "list.h"
#include "log.h"
#include "mime-types.h"
#include "net.h"
#include "platform.h" /* tr_lockLock() */
#include "platform-quota.h" /* tr_device_info_create(), tr_device_info_get_free_space(), tr_device_info_free() */
@ -2254,3 +2255,38 @@ void tr_net_init(void)
initialized = true;
}
}
/// mime-type
static int compareSuffix(void const* va, void const* vb)
{
char const* suffix = va;
struct mime_type_suffix const* entry = vb;
return tr_strcmp0(suffix, entry->suffix);
}
char const* tr_get_mime_type_for_filename(char const* filename)
{
struct mime_type_suffix const* info = NULL;
char const* in = strrchr(filename, '.');
if ((in != NULL) && (strlen(++in) <= MIME_TYPE_SUFFIX_MAXLEN))
{
char lowercase_suffix[MIME_TYPE_SUFFIX_MAXLEN + 1];
char* out = lowercase_suffix;
while (*in != '\0')
{
*out++ = tolower((unsigned char)*in++);
}
*out = '\0';
info = bsearch(lowercase_suffix,
mime_type_suffixes,
TR_N_ELEMENTS(mime_type_suffixes),
sizeof(*mime_type_suffixes),
compareSuffix);
}
return info != NULL ? info->mime_type : NULL;
}

View File

@ -60,6 +60,8 @@ char const* tr_strip_positional_args(char const* fmt);
#define TR_N_ELEMENTS(ary) (sizeof(ary) / sizeof(*(ary)))
char const* tr_get_mime_type_for_filename(char const* filename);
/**
* @brief Rich Salz's classic implementation of shell-style pattern matching for ?, \, [], and * characters.
* @return 1 if the pattern matches, 0 if it doesn't, or -1 if an error occured

View File

@ -17,14 +17,14 @@
#include <QFileIconProvider>
#include <QFileInfo>
#include <QIcon>
#include <QMimeDatabase>
#include <QObject>
#include <QPainter>
#include <QStyle>
#ifdef _WIN32
#include <QPixmapCache>
#include <QtWin>
#else
#include <QMimeDatabase>
#include <QMimeType>
#endif
#include <libtransmission/transmission.h>
@ -75,6 +75,58 @@ QIcon IconCache::guessMimeIcon(QString const& filename, QIcon fallback) const
return icon;
}
QIcon IconCache::getMimeTypeIcon(QString const& mime_type_name, bool multifile) const
{
auto& icon = (multifile ? name_to_emblem_icon_ : name_to_icon_)[mime_type_name];
if (!icon.isNull())
{
return icon;
}
if (!multifile)
{
QMimeDatabase mime_db;
auto const type = mime_db.mimeTypeForName(mime_type_name);
icon = QIcon::fromTheme(type.iconName());
if (icon.isNull())
{
icon = QIcon::fromTheme(type.genericIconName());
}
if (icon.isNull())
{
icon = file_icon_;
}
return icon;
}
auto const mime_icon = getMimeTypeIcon(mime_type_name, false);
for (auto const& size : { QSize(24, 24), QSize(32, 32), QSize(48, 48) })
{
// upper left corner
auto const folder_size = size / 2;
auto const folder_rect = QRect(QPoint(), folder_size);
// fullsize
auto const mime_size = size;
auto const mime_rect = QRect(QPoint(), mime_size);
// build the icon
auto pixmap = QPixmap(size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHints(QPainter::SmoothPixmapTransform);
painter.drawPixmap(folder_rect, folder_icon_.pixmap(folder_size));
painter.drawPixmap(mime_rect, mime_icon.pixmap(mime_size));
icon.addPixmap(pixmap);
}
return icon;
}
/***
****
***/
@ -132,11 +184,11 @@ QIcon IconCache::getMimeIcon(QString const& filename) const
return {};
}
QIcon& icon = icon_cache_[ext];
QIcon& icon = ext_to_icon_[ext];
if (icon.isNull()) // cache miss
{
QMimeDatabase mime_db;
QMimeType type = mime_db.mimeTypeForFile(filename, QMimeDatabase::MatchExtension);
auto const type = mime_db.mimeTypeForFile(filename, QMimeDatabase::MatchExtension);
if (icon.isNull())
{
icon = QIcon::fromTheme(type.iconName());

View File

@ -30,6 +30,7 @@ public:
QIcon folderIcon() const { return folder_icon_; }
QIcon fileIcon() const { return file_icon_; }
QIcon guessMimeIcon(QString const& filename, QIcon fallback = {}) const;
QIcon getMimeTypeIcon(QString const& mime_type, bool multifile) const;
protected:
IconCache();
@ -38,11 +39,14 @@ private:
QIcon const folder_icon_;
QIcon const file_icon_;
mutable std::unordered_map<QString, QIcon> name_to_icon_;
mutable std::unordered_map<QString, QIcon> name_to_emblem_icon_;
#if defined(_WIN32)
void addAssociatedFileIcon(QFileInfo const& file_info, UINT icon_size, QIcon& icon) const;
#else
mutable std::unordered_map<QString, QIcon> icon_cache_;
mutable std::unordered_set<QString> suffixes_;
mutable std::unordered_map<QString, QIcon> ext_to_icon_;
QIcon getMimeIcon(QString const& filename) const;
#endif
};

View File

@ -539,11 +539,13 @@ std::vector<std::string_view> const& Session::getKeyNames(TorrentProperties prop
if (names.empty())
{
// unchanging fields needed by the main window
static auto constexpr MainInfoKeys = std::array<tr_quark, 6>{
static auto constexpr MainInfoKeys = std::array<tr_quark, 8>{
TR_KEY_addedDate,
TR_KEY_downloadDir,
TR_KEY_file_count,
TR_KEY_hashString,
TR_KEY_name,
TR_KEY_primary_mime_type,
TR_KEY_totalSize,
TR_KEY_trackers,
};

View File

@ -165,21 +165,7 @@ QIcon Torrent::getMimeTypeIcon() const
{
if (icon_.isNull())
{
auto const& files = files_;
auto const& icon_cache = IconCache::get();
if (files.size() > 1)
{
icon_ = icon_cache.folderIcon();
}
else if (files.size() == 1)
{
icon_ = icon_cache.guessMimeIcon(files.at(0).filename, icon_cache.fileIcon());
}
else
{
icon_ = icon_cache.guessMimeIcon(name(), icon_cache.folderIcon());
}
icon_ = IconCache::get().getMimeTypeIcon(primary_mime_type_, file_count_ > 1);
}
return icon_;
@ -220,6 +206,7 @@ Torrent::fields_t Torrent::update(tr_quark const* keys, tr_variant const* const*
HANDLE_KEY(eta, eta, ETA)
HANDLE_KEY(fileStats, files, FILES)
HANDLE_KEY(files, files, FILES)
HANDLE_KEY(file_count, file_count, FILE_COUNT)
HANDLE_KEY(hashString, hash, HASH)
HANDLE_KEY(haveUnchecked, have_unchecked, HAVE_UNCHECKED)
HANDLE_KEY(haveValid, have_verified, HAVE_VERIFIED)
@ -239,6 +226,7 @@ Torrent::fields_t Torrent::update(tr_quark const* keys, tr_variant const* const*
HANDLE_KEY(percentDone, percent_done, PERCENT_DONE)
HANDLE_KEY(pieceCount, piece_count, PIECE_COUNT)
HANDLE_KEY(pieceSize, piece_size, PIECE_SIZE)
HANDLE_KEY(primary_mime_type, primary_mime_type, PRIMARY_MIME_TYPE)
HANDLE_KEY(queuePosition, queue_position, QUEUE_POSITION)
HANDLE_KEY(rateDownload, download_speed, DOWNLOAD_SPEED)
HANDLE_KEY(rateUpload, upload_speed, UPLOAD_SPEED)
@ -282,7 +270,8 @@ Torrent::fields_t Torrent::update(tr_quark const* keys, tr_variant const* const*
{
switch (key)
{
case TR_KEY_name:
case TR_KEY_file_count:
case TR_KEY_primary_mime_type:
{
icon_ = {};
break;
@ -290,7 +279,6 @@ Torrent::fields_t Torrent::update(tr_quark const* keys, tr_variant const* const*
case TR_KEY_files:
{
icon_ = {};
for (int i = 0; i < files_.size(); ++i)
{
files_[i].index = i;

View File

@ -561,6 +561,7 @@ public:
ERROR_STRING,
ETA,
FAILED_EVER,
FILE_COUNT,
FILES,
HASH,
HAVE_UNCHECKED,
@ -582,6 +583,7 @@ public:
PERCENT_DONE,
PIECE_COUNT,
PIECE_SIZE,
PRIMARY_MIME_TYPE,
QUEUE_POSITION,
RECHECK_PROGRESS,
SEED_IDLE_LIMIT,
@ -642,6 +644,7 @@ private:
uint64_t desired_available_ = {};
uint64_t downloaded_ever_ = {};
uint64_t failed_ever_ = {};
uint64_t file_count_ = {};
uint64_t have_unchecked_ = {};
uint64_t have_verified_ = {};
uint64_t left_until_done_ = {};
@ -655,6 +658,7 @@ private:
double recheck_progress_ = {};
double seed_ratio_limit_ = {};
QString primary_mime_type_;
QString comment_;
QString creator_;
QString download_dir_;

View File

@ -442,3 +442,12 @@ TEST_F(UtilsTest, env)
s = makeString(tr_env_get_string(test_key, "c"));
EXPECT_EQ("135", s);
}
TEST_F(UtilsTest, mimeTypes)
{
EXPECT_STREQ("audio/x-flac", tr_get_mime_type_for_filename("music.flac"));
EXPECT_STREQ("audio/x-flac", tr_get_mime_type_for_filename("music.FLAC"));
EXPECT_STREQ("video/x-msvideo", tr_get_mime_type_for_filename(".avi"));
EXPECT_STREQ("video/x-msvideo", tr_get_mime_type_for_filename("/path/to/FILENAME.AVI"));
EXPECT_EQ(nullptr, tr_get_mime_type_for_filename("music.ajoijfeisfe"));
}