65.9K
CodeProject 正在变化。 阅读更多。
Home

电影和电视节目名称解析器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2019年3月22日

公共领域

9分钟阅读

viewsIcon

14315

downloadIcon

226

用于自动识别电影和电视节目的单文件、集成到 Shell 的简便解决方案。

引言

这段 VBScript 代码是为一种相当具体purpose编写的,因此它可能无法在原始形式中很好地重用,尽管我相信通过一些重写,它对于其他目的,或者至少是它的某些部分/子程序,可能会非常有用。

为了解释这段代码的 purpose,我必须先解释我面临的问题/挑战。我电脑上安装了一个媒体服务器,它创建了一个电影和电视节目库,这些库位于硬盘的特定文件夹中,我通过命名约定对它们进行排序和分类。

我举个例子。如果我下载了一部新电影或一个节目剧集,它通常会被命名为“Series.name.S01E01.encodername.x264-WebRIP.mkv”。我希望将其命名为“Series Name – S01E01 – Episode Name.mkv”,然后将其放入文件夹D:\Series\Series Name\Season 1\。所以,我首先需要查找剧集名称,然后重命名文件,然后手动将其复制/移动到目标文件夹(如果不存在则创建该文件夹)。我想自动化这个过程——而且,我还希望将它保存在一个可执行文件中,这个文件易于编辑、传输、阅读,无需(实际)安装,并且可以直接从 shell 对文件或文件夹运行。所以我决定用 VBscript 来实现,将其打包成一个 CMD 文件,该文件从内部调用脚本代码(脚本是写在批处理文件里的)。

必备组件

为了获取有关媒体(电影或节目/剧集)的信息,我使用了 www.omdbapi.com,这是一个基于 RESTful Web 服务的开放(免费)电影数据库 API。要使用该 API,您需要创建一个 API 密钥,免费使用每天限制为 1000 次请求。

要获得无限 API 密钥,您需要成为该页面的赞助者(发送捐款)。只需访问此网页,输入您的电子邮件并按照说明操作

Using the Code

有三种可能的调用选项

  • 安装resolve_video.cmd -install
  • 卸载resolve_video.cmd -uninstall
  • 文件处理resolve_video.cmd <file_or_folder_path> <series/movie> <OMDB_API_key>

重要提示:对于安装和卸载过程,命令提示符应以管理员权限运行(以管理员身份运行),否则将不会发生任何事情。

安装

“安装”过程并非真正的安装——它实际上不会复制任何文件(因为这只是一个批处理脚本文件,没有安装文件)——它只是创建了必要的注册表项,以便可以直接从 shell 上运行该批处理文件。

一旦批处理文件以 -install 选项运行,它将首先要求提供 OMDB API 密钥,然后它将输出需要创建的必要注册表项,以便能够从 shell 运行——用户在这些条目实际写入注册表之前需要确认。

卸载

卸载过程将删除在安装过程中写入注册表的所有条目;它不会删除任何文件。与安装一样,它会首先提示用户列出将被删除的注册表条目。

文件处理

文件处理部分需要三个参数;第一个是用于解析的文件夹或文件的文件路径,第二个是字符串 movieseries,取决于您要在 OMDB 上搜索哪一个。第三个参数是 OMDB API 密钥。

如果已经完成了注册表“安装”,则在注册表中已写入了所有正确参数的批处理文件的正确调用。但是,如果您独立运行批处理文件,则需要显式提供参数。

在下一节中,将解释文件处理部分的主要子程序。

主要子程序

代码主要分为四个部分

  1. 收集要处理的文件(如果脚本在文件夹上运行,则很重要)—— CollectFiles
  2. 解析每个文件的文件名(提取用于查询 OMDB 的信息)—— ParseFilename
  3. 查询 OMDB 并读取响应—— QueryOMDBParseOMDB
  4. 重命名并复制文件—— GetNewFilenameCopyFiles

CollectFiles

此子程序将首先检测给定路径是文件还是文件夹,如果是文件夹,它将从其中“收集”所有具有 Const c_valid_ext(Function CheckExtension)中扩展名的文件。它还会查找以视频文件名为开头字幕文件,例如,对于文件“Batman.Begins.2005.BRRIP.1080p.mkv”,它将查找名为“Batman.Begins.2005.BRRIP.1080p……*sometext*……..”(Function FindSubtitle)的字幕文件。字幕文件需要有一个有效的扩展名(Const c_sub_ext 中的一个)。

此子程序的输出是

  • String 数组 files(视频文件路径)
  • Dictionary(string,string) extensions(文件的扩展名,视频文件路径是键)
  • Dictionary(string,string) subtitles(字幕文件路径,视频文件路径是键)

ParseFilename

ParseFilename 将根据调用时的第二个参数调用相应的 Sub——对于 series,调用 ParseSeries;对于 movie,调用 ParseMovie

ParseSeries

假定一个节目剧集通常的文件名如下所示

<series_name>…<SyyEzz|yyxzz>….

点可以是任何非字母数字字符。在 season/episode string 之后也可能出现剧集名称,但 Sub 将忽略它,因为正确的剧集名称将在稍后通过 OMDB API 搜索。

文件名将首先通过任何非字母数字字符作为分隔符分解为 string 数组。接下来,读取每个 string,并且在季节/剧集 string 之前找到的所有内容都将被视为系列标题的一部分,而模式 SyyEzzyyxzz 将被识别为季节/剧集编号。

输出是 string series_name,以及整数 season_nrepisode_nr

ParseMovie

ParseMovie sub 的工作方式与 ParseSeries 相同,只是它查找发布年份而不是季节/剧集模式。任何作为 4 位数字且介于 1950 和当前年份之间的单词都被视为发布年份,否则被视为电影标题的一部分。1950 是随意选择的,您可以输入您喜欢的任何年份。

输出是两个 stringname(电影标题)和 yyyy(发布年份)。

QueryOMDB & ParseOMDB

一旦我们有了系列名称 + season_nr/episode_nr 或电影名称 + 发布年份,我们就使用提供的 API 密钥查询 OMDB API。

以下是 Supernatural 电视节目第 14 季的 API 请求和响应示例(您需要输入一个有效的 <<API key>>

请求: http://www.omdbapi.com/?apikey=<<API_KEY>>&t=supernatural&type=series&season=14

响应

{"Title":"Supernatural","Season":"14","totalSeasons":"14","Episodes":[{"Title":"Stranger in a Strange Land","Released":"2018-10-11","Episode":"1","imdbRating":"8.1","imdbID":"tt8226756"},{"Title":"Gods and Monsters","Released":"2018-10-18","Episode":"2","imdbRating":"8.7","imdbID":"tt8408494"},{"Title":"Episode #14.3","Released":"2018-10-25","Episode":"3","imdbRating":"N/A","imdbID":"tt8408498"},{"Title":"Mint Condition","Released":"2018-11-01","Episode":"4","imdbRating":"8.9","imdbID":"tt8408504"},{"Title":"Nightmare Logic","Released":"2018-11-08","Episode":"5","imdbRating":"N/A","imdbID":"tt8408508"},{"Title":"Optimism","Released":"2018-11-15","Episode":"6","imdbRating":"8.5","imdbID":"tt8408506"},{"Title":"Episode #14.7","Released":"2018-11-29","Episode":"7","imdbRating":"N/A","imdbID":"tt8408502"},{"Title":"Byzantium","Released":"2018-12-06","Episode":"8","imdbRating":"9.0","imdbID":"tt8408500"},{"Title":"Episode #14.9","Released":"2018-12-13","Episode":"9","imdbRating":"N/A","imdbID":"tt8408512"},{"Title":"Nihilism","Released":"2019-01-17","Episode":"10","imdbRating":"9.1","imdbID":"tt8408510"},{"Title":"Damaged Goods","Released":"2019-01-24","Episode":"11","imdbRating":"8.9","imdbID":"tt8962434"},{"Title":"Prophet and Loss","Released":"2019-01-31","Episode":"12","imdbRating":"8.7","imdbID":"tt8962440"},{"Title":"Lebanon","Released":"2019-02-07","Episode":"13","imdbRating":"9.6","imdbID":"tt8962446"},{"Title":"Ouroboros","Released":"2019-03-07","Episode":"14","imdbRating":"9.4","imdbID":"tt9271138"},{"Title":"Peace of Mind","Released":"2019-03-14","Episode":"15","imdbRating":"9.2","imdbID":"tt9271140"},{"Title":"Don't Go in the Woods","Released":"2019-03-21","Episode":"16","imdbRating":"8.5","imdbID":"tt9271142"}],"Response":"True"}

API 的详细使用说明可以在主页上找到

对于 Http 请求,此脚本使用 Microsoft.XmlHttp 类型的对象

Public http: Set http = CreateObject("Microsoft.XmlHttp")
http.open "GET", url, False
http.send ""
respons = http.responseText

响应在 sub ParseOMDBResponse 中根据以 c_resp 开头的常量进行解析,这些常量代表 response string 中的关键字。

Const c_movie="movie"
Const c_series="series"
Const c_resp="Response:"
Const c_resp_true="True"
Const c_resp_false="False"
Const c_resp_title="Title:"
Const c_resp_year="Year:"
Const c_resp_totalseasons="totalSeasons:"
Const c_resp_episodes="Episodes:"
Const c_resp_ep_title="Title:"
Const c_resp_ep_nr="Episode:"

假设没有错误(c_resp_false),输出是:name(系列或电影标题),episode_name(剧集标题 - 对于系列),yyyy(发布年份),maxseasons(总季数)。

GetNewFilename & CopyFiles

一旦我们有了所有变量,我们就可以生成新的文件名。这在 GetNewFilename sub 中完成。

sub 使用代码中提供的掩码(常量 c_mask_seriesc_mask_movie)为处理过的文件创建新的名称/路径。

目标文件夹在常量 c_destination_folder_moviec_destination_folder_series 中指定。

我使用了这些,您应该在自己的系列/电影文件夹中放入自己的掩码和路径。

Const c_mask_series="%name%\Season %season_nr_1%\%name% - S%season_nr%E%episode_nr% - %episode_name%"
Const c_mask_movie="%name% (%yyyy%)\%name%"
Const c_destination_folder_movie="V:\MOVIES\"
Const c_destination_folder_series="V:\SERIES\"

(被 %% 包围的 string 会被替换为相应的变量。)

GetNewFilename 过程将结合适当的目标文件夹和掩码来创建每个文件的新路径。它会将新文件名保存在字典 new_filename 中(原始文件名是键)。新字幕文件名将保存在字典 new_sub 中。

旧文件和新文件路径首先会显示给用户,以确认他们是否希望复制文件。确认后,将调用 CopyFiles 子程序,该子程序将为每个文件调用 FileRobocopy 函数和 TrackCopyProgress 子程序。

FileRobocopy 函数将首先创建一个新的批处理文件,该文件将首先调用 Windows 的 robust copy(robocopy)命令将给定文件复制到给定目标,然后使用 Windows 命令 ren 将新文件重命名为新文件名。robocopy 函数的输出将被路由到一个文件,然后该函数将 robocopy 输出文件名返回给 CopyFiles 过程,然后该过程通过读取 robocopy 日志文件来跟踪进度。

robocopy 批处理完成后,它将自行删除。

TrackCopyProgress

TrackCopyProgress 将连续读取 robocopy 日志文件,直到达到 100% 行。该子程序的输出是一个由 Unicode 字符模拟的进度条(由函数调用 ChrW(9608)ChrW(9618) 返回)。

Sub PrintProgress(perc, length)
   Dim progres
   WScript.stdout.Write chr(13)
   tmp = ChrW(9608)
   For i = 1 To length
      If i > CInt(length * perc / 100) Then tmp = ChrW(9618)
      progres = progres & tmp
   Next
   WScript.stdout.Write progres & " ( " & CStr(CInt(perc)) & "% )"
End Sub

附加文件

批处理文件在处理过程中会创建几个临时文件,其中大多数一旦不再需要就会立即被删除。所有附加的(临时)文件将保存在与批处理文件相同的目录中。

批处理日志文件

这是批处理的每次处理后唯一不会被删除的附加文件。该文件将与批处理脚本同名,但扩展名为 .log——通常名称将是 resolve_video.log。此文件是整个批处理过程的日志——Log sub 写入处理步骤,带有时间戳和调用过程的名称。每次文件都会被重写。

Sub Log(msg)
   logfile.WriteLine TimeStamp & Chr(9) & msg
End Sub

Copy File Temp Batch

对于每个需要复制的文件,FileRobocopy 函数将创建一个由三个命令组成的批处理:robocopy(用于复制文件)、ren(用于重命名复制的文件)和 del(用于删除临时批处理文件)。

batchfile.WriteLine "robocopy """ & fromwhere & """ """ & towhere & """ """ & _
            fso.GetFile(old_filename).Name & """ > """ & robocopy_log & """"
batchfile.WriteLine "ren """ & towhere & "\" & filename & """ """ & _
            Replace(new_filename, towhere & "\", "") & """"
batchfile.WriteLine "del """ & batchfile_name & """"

Robocopy Output File

robocopy 命令的输出被路由到一个文件,该文件使用创建时间戳命名(以避免同时出现多个同名文件)。

此文件由 TrackCopyProgress 子程序读取,以了解实际的复制进度。(此文件的最后一行将始终包含复制进度的百分比——0.1% 到 100%。)

Path of the Batch Temp

在调用 VBscript 之前,会立即创建一个文件——批处理将其路径写入文件“cmd.path.tmp”。该文件在脚本开始时被读取,然后立即删除。

在“安装”过程中,此名称用于在从 shell 调用时写入正确的批处理路径(写入注册表)。

© . All rights reserved.