sqliteで管理してみた(3) - ファイル検出と補足

はじめに

ここではsqliteとは直接の関係はありません。録画ファイルが保存されている環境において新規に作成されたTSファイルや削除、移動等、ファイルを検出してデータベース上のデータと同期を取るための、どちらかというとファイル検出に関する内容となります。

検出すべき内容

録画を常に実施している環境であれば定期的にファイルが生成されます。古い録画データを削除しているかも知れませんし、別フォルダに移動しているかも知れません。

ファイル検出/削除

sqliteで管理してみた(1)で紹介した構成例となります。

ProgFolderテーブル
ProgEntryテーブル

ProgFolderのNo = 4にはProgEntryのNo = 2, 3, 4が含まれています。つまり、2,3,4以外のファイルを検出した場合は新規であるということになります。逆に2,3,4のファイルが存在していない場合に存在していないのが削除されたファイルとなります。

フォルダの検出/削除

ファイルと同様ですがProgFolderのNo = 2にはSubFolderとしてNo = 4を指示しています。'F:/RecFolder/ドラマ'配下に'朝ドラ'以外のフォルダがあれば新規フォルダとなり、'朝ドラ'フォルダが無ければ削除されたフォルダとなります。

フォルダの最終書き込みを活用

全てのフォルダやファイルを調べるには時間も負荷もかかります。ProgFolderにWriteTimeを活用します。要はWriteTimeに変更がなければそのフォルダは変更が加えられていないという判断をします。

public static int ScanFolder(string dirname)
{
    DirectoryInfo dInfo = new DirectoryInfo(dirname);
    ProgFolder pfInfo = Database.GetFolderInfo(dirname.Replace("\\""/"));

    // このフォルダのSubFoldersとNodes情報格納先
    List<ProgFolder> dbfolder = new List<ProgFolder>();
    List<ProgEntry> dbentry = new List<ProgEntry>();

    if (pfInfo == null)
    {
        // データベース上には存在しないので器だけ作成
        pfInfo = new ProgFolder(dirname);
    }
    else
    {
        if (pfInfo.SubFolders.Count != 0)
        {
            // SubFolders情報取得
            dbfolder = Database.GetFolderInfo(pfInfo.SubFolders.ToArray());
        }

        if (pfInfo.Nodes.Count != 0)
        {
            // Nodes情報取得
            dbentry = Database.GetEntryInfo(pfInfo.Nodes.ToArray());
        }
    }

    // フォルダ最終書き込み時間をDBと比較して違いがあれば変更されている
    if (pfInfo.LastWrite == Utils.DateTimeToUnixTime(dInfo.LastWriteTime))
    {
        foreach(var subfolder in dInfo.GetDirectories())
        {
            // このフォルダに変更はないので特に返値もいらない
            ScanFolder(subfolder.FullName);
        }
        return pfInfo.No;
    }
    else
    {
        // 新規、削除ファイル/フォルダ検出処理
    }
}

肝心のファイル/フォルダ検出処理では、新規ファイルとフォルダ、削除ファイルとフォルダ、サブディレクトリ配下も検出処理を実施する必要があります。

新規フォルダ検出

List<string> remain_folders = new List<string>();

// 新規フォルダ検出
foreach (var subfolder in dInfo.GetDirectories())
{
    bool found = false;
    foreach (var dbitem in dbfolder)
    {
        if (dbitem.Name == subfolder.FullName.Replace("\\""/"))
        {
            found = true;
            break;
        }
    }
    if (found)
    {
        // データベースと実フォルダにも存在しているので一旦remain_foldersに追加
        remain_folders.Add(subfolder.FullName);      
    }
    else
    {
        // DB上に存在しなかったのでSubFoldersに追加
        pfInfo.SubFolders.Add(ScanFolder(subfolder.FullName));
    }
}

削除フォルダ検出

// 削除フォルダ検出
foreach (var dbitem in dbfolder)
{
    bool found = false;
    foreach (var subfolder in dInfo.GetDirectories())
    {
        if (dbitem.Name == subfolder.FullName.Replace("\\""/"))
        {
            found = true;
            break;
        }
    }
    if (!found)
    {
        RemoveAllFolder(dbitem);
        pfInfo.SubFolders.Remove(dbitem.No);
    }
}

フォルダ削除を検出したらサブフォルダやファイルエントリを削除するようにするためにRemoveAllFolderで外だししました。

private static void RemoveAllFolder(ProgFolder folder)
{
    if (folder.Nodes.Count != 0)
    {
        foreach (var item in Database.GetEntryInfo(folder.Nodes.ToArray()))
        {
            Database.DeleteEntry(item);
        }
    }
    if (folder.SubFolders.Count != 0)
    {
        foreach (var item in Database.GetFolderInfo(folder.SubFolders.ToArray()))
        {
            RemoveAllFolder(item);
        }
    }
    Database.DeleteFolder(folder);
}

新規ファイル検出

新規ファイルを検出してパラレル処理することにしました。今の状態だとパラレル処理する必要は全くありませんが、番組情報取得やサムネイル作成に時間を要する可能性があるためです。

// 新規ファイル検出
List<FileInfo> NewEntry = new List<FileInfo>();
foreach (var entry in Directory.EnumerateFiles(dirname))
{
    FileInfo fInfo = new FileInfo(entry);
    if (!StoreExtenstion.Contains(Path.GetExtension(entry))) { continue; }
    bool found = false;
    foreach (var dentry in dbentry)
    {
        if (fInfo.Name == dentry.Name)
        {
            found = true;
            break;
        }
    }
    if (!found) { NewEntry.Add(fInfo); }
}

ConcurrentBag<ProgEntry> resultCollection = new ConcurrentBag<ProgEntry>();
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 4;

ParallelLoopResult result = Parallel.ForEach(NewEntry, options, file =>
{
    ProgEntry buffer = new ProgEntry(file.Name);
    // 番組情報取得処理を入れる
    // サムネイル作成処理を入れる
    resultCollection.Add(buffer);
});

if (result.IsCompleted)
{
    foreach (var FileEntry in resultCollection)
    {
        int fileNo = Database.InsertEntry(FileEntry);
        if (fileNo != -1)
        {
            if (!pfInfo.Nodes.Contains(fileNo)) { pfInfo.Nodes.Add(fileNo); }
        }
    }
}

削除ファイル検出

// 削除ファイル検出
foreach (var dentry in dbentry)
{
    bool found = false;
    foreach (var entry in dInfo.GetFiles())
    {
        if (!StoreExtenstion.Contains(Path.GetExtension(entry.FullName))) { continue; }
        if (entry.Name == dentry.Name)
        {
            found = true;
            break;
        }
    }
    if (!found)
    {
        if (Database.DeleteEntry(dentry) == 1)
        {
            pfInfo.Nodes.Remove(dentry.No);
        }
    }
}

サブフォルダ処理

// サブフォルダ検索
foreach(var subfolder in remain_folders)
{
    ScanFolder(subfolder);
}

pfInfo.LastWrite = Utils.DateTimeToUnixTime(dInfo.LastWriteTime);
return Database.InsertFolder(pfInfo);

最後にLastWriteを更新して処理完了とします。新規と削除検出で2回ループさせていますが、もうちょっと考えて1回のループで処理できる方にした方が良い気がしています。

TSファイルとTSファイル以外の処理

何らかの番組情報がある時

TSファイルは以前実施した番組情報からデータを取得して、TSファイル以外は、拡張子を除いた同じ名称のファイルが存在していればその番組情報を複製することにします。

例.
地方の果てまでいきたいな.ts
地方の果てまでいきたいな.mp4

上記のような場合は拡張子が異なりますが同じ扱いにします。

番組情報がない時

過去にエンコード済みのファイルや元々tsファイルではない動画ファイルには番組情報はありません。これをどのように扱うかを決めておきたいと思います。まず、取得可能な情報をいくつか抜粋すると、

  • 動画長さ

  • 動画や音声のフォーマット情報

  • 字幕有無、チャプター有無

次回これらを少し検討しますが、とりあえず番組情報ある場合を取り込んでみます。

この記事が気に入ったらサポートをしてみませんか?