Migration towards TypeScript
This is going to be 1.1.0. Remaining TODOs: * "npm run tsc" should generate declarations * Add (restrictive) TSLint configuration * Add support for TSLint in "npm test"
This commit is contained in:
parent
d01b204cce
commit
9ff6398aee
7
.gitignore
vendored
7
.gitignore
vendored
@ -1 +1,8 @@
|
|||||||
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
obj/
|
||||||
|
typings/
|
||||||
|
*.dat
|
||||||
|
*.dll
|
||||||
|
*.suo
|
||||||
|
*.tmp
|
||||||
60
.jscsrc
60
.jscsrc
@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"disallowEmptyBlocks": true,
|
|
||||||
"disallowImplicitTypeConversion": ["binary", "boolean", "numeric", "string"],
|
|
||||||
"disallowKeywords": ["delete", "with"],
|
|
||||||
"disallowKeywordsOnNewLine": ["catch", "else", "finally"],
|
|
||||||
"disallowMixedSpacesAndTabs": true,
|
|
||||||
"disallowMultipleLineBreaks": true,
|
|
||||||
"disallowMultipleLineStrings": true,
|
|
||||||
"disallowMultipleVarDecl": true,
|
|
||||||
"disallowNewlineBeforeBlockStatements": true,
|
|
||||||
"disallowPaddingNewlinesInBlocks": true,
|
|
||||||
"disallowQuotedKeysInObjects": "allButReserved",
|
|
||||||
"disallowSpaceAfterObjectKeys": true,
|
|
||||||
"disallowSpaceAfterPrefixUnaryOperators": true,
|
|
||||||
"disallowSpaceBeforePostfixUnaryOperators": true,
|
|
||||||
"disallowSpacesInCallExpression": true,
|
|
||||||
"disallowSpacesInFunction": {"beforeOpeningRoundBrace": true},
|
|
||||||
"disallowSpacesInsideArrayBrackets": true,
|
|
||||||
"disallowSpacesInsideObjectBrackets": true,
|
|
||||||
"disallowSpacesInsideParentheses": true,
|
|
||||||
"disallowTrailingComma": true,
|
|
||||||
"disallowTrailingWhitespace": true,
|
|
||||||
"disallowYodaConditions": true,
|
|
||||||
"jsDoc": {
|
|
||||||
"checkAnnotations": "closurecompiler",
|
|
||||||
"checkParamNames": true,
|
|
||||||
"checkRedundantAccess": true,
|
|
||||||
"checkRedundantParams": true,
|
|
||||||
"checkReturnTypes": true,
|
|
||||||
"checkTypes": "strictNativeCase",
|
|
||||||
"enforceExistence": true,
|
|
||||||
"leadingUnderscoreAccess": true,
|
|
||||||
"requireParamTypes": true,
|
|
||||||
"requireReturnTypes": true
|
|
||||||
},
|
|
||||||
"maximumLineLength": 80,
|
|
||||||
"plugins": ["jscs-jsdoc"],
|
|
||||||
"requireBlocksOnNewline": true,
|
|
||||||
"requireCamelCaseOrUpperCaseIdentifiers": true,
|
|
||||||
"requireCapitalizedConstructors": true,
|
|
||||||
"requireCommaBeforeLineBreak": true,
|
|
||||||
"requireDotNotation": true,
|
|
||||||
"requireFunctionDeclarations": true,
|
|
||||||
"requireLineFeedAtFileEnd": true,
|
|
||||||
"requireOperatorBeforeLineBreak": true,
|
|
||||||
"requireParenthesesAroundIIFE": true,
|
|
||||||
"requireSpaceAfterBinaryOperators": true,
|
|
||||||
"requireSpaceAfterKeywords": ["case", "catch", "do", "else", "for", "if", "return", "switch", "try", "typeof", "void", "while", "with"],
|
|
||||||
"requireSpaceAfterLineComment": true,
|
|
||||||
"requireSpaceBeforeBinaryOperators": true,
|
|
||||||
"requireSpaceBeforeBlockStatements": true,
|
|
||||||
"requireSpaceBeforeObjectValues": true,
|
|
||||||
"requireSpacesInConditionalExpression": true,
|
|
||||||
"requireSpacesInFunction": {"beforeOpeningCurlyBrace": true},
|
|
||||||
"safeContextKeyword": ["that"],
|
|
||||||
"validateIndentation": 2,
|
|
||||||
"validateParameterSeparator": ", ",
|
|
||||||
"validateQuoteMarks": "'",
|
|
||||||
"validateLineBreaks": "LF"
|
|
||||||
}
|
|
||||||
79
.jshintrc
79
.jshintrc
@ -1,79 +0,0 @@
|
|||||||
{
|
|
||||||
"bitwise" : false,
|
|
||||||
"camelcase" : false,
|
|
||||||
"curly" : false,
|
|
||||||
"eqeqeq" : true,
|
|
||||||
"es3" : false,
|
|
||||||
"forin" : true,
|
|
||||||
"freeze" : true,
|
|
||||||
"immed" : true,
|
|
||||||
"indent" : 4,
|
|
||||||
"latedef" : "nofunc",
|
|
||||||
"newcap" : true,
|
|
||||||
"noarg" : true,
|
|
||||||
"noempty" : true,
|
|
||||||
"nonbsp" : true,
|
|
||||||
"nonew" : true,
|
|
||||||
"plusplus" : true,
|
|
||||||
"quotmark" : "single",
|
|
||||||
"undef" : true,
|
|
||||||
"unused" : true,
|
|
||||||
"singleGroups" : false,
|
|
||||||
"strict" : true,
|
|
||||||
"maxparams" : 5,
|
|
||||||
"maxdepth" : 5,
|
|
||||||
"maxstatements": 25,
|
|
||||||
"maxcomplexity": 5,
|
|
||||||
"maxlen" : 80,
|
|
||||||
|
|
||||||
"asi" : false,
|
|
||||||
"boss" : false,
|
|
||||||
"debug" : false,
|
|
||||||
"eqnull" : false,
|
|
||||||
"evil" : false,
|
|
||||||
"expr" : false,
|
|
||||||
"esnext" : false,
|
|
||||||
"funcscope" : false,
|
|
||||||
"globalstrict" : false,
|
|
||||||
"iterator" : false,
|
|
||||||
"lastsemic" : false,
|
|
||||||
"laxbreak" : false,
|
|
||||||
"laxcomma" : false,
|
|
||||||
"loopfunc" : false,
|
|
||||||
"maxerr" : 50,
|
|
||||||
"moz" : false,
|
|
||||||
"multistr" : false,
|
|
||||||
"notypeof" : false,
|
|
||||||
"noyield" : false,
|
|
||||||
"proto" : false,
|
|
||||||
"scripturl" : false,
|
|
||||||
"scope" : false,
|
|
||||||
"shadow" : false,
|
|
||||||
"sub" : false,
|
|
||||||
"supernew" : false,
|
|
||||||
"validthis" : false,
|
|
||||||
"withstmt" : false,
|
|
||||||
|
|
||||||
"browser" : false,
|
|
||||||
"browserify" : false,
|
|
||||||
"couch" : false,
|
|
||||||
"devel" : false,
|
|
||||||
"dojo" : false,
|
|
||||||
"jasmine" : false,
|
|
||||||
"jquery" : false,
|
|
||||||
"mootools" : false,
|
|
||||||
"mocha" : false,
|
|
||||||
"node" : true,
|
|
||||||
"nonstandard" : false,
|
|
||||||
"phantom" : false,
|
|
||||||
"prototypejs" : false,
|
|
||||||
"qunit" : false,
|
|
||||||
"rhino" : false,
|
|
||||||
"shelljs" : false,
|
|
||||||
"typed" : false,
|
|
||||||
"worker" : false,
|
|
||||||
"wsh" : false,
|
|
||||||
"yui" : false,
|
|
||||||
|
|
||||||
"globals" : []
|
|
||||||
}
|
|
||||||
12
.npmignore
12
.npmignore
@ -1 +1,11 @@
|
|||||||
extras/
|
node_modules/
|
||||||
|
obj/
|
||||||
|
src/
|
||||||
|
typings/
|
||||||
|
*.dat
|
||||||
|
*.dll
|
||||||
|
*.map
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.suo
|
||||||
|
*.tmp
|
||||||
@ -45,6 +45,7 @@ decision prior to using this application.
|
|||||||
* Binary runner for `npm`
|
* Binary runner for `npm`
|
||||||
* Windows examples with a .bat for ease of use.
|
* Windows examples with a .bat for ease of use.
|
||||||
* Publish to `npm` with a fixed package.json.
|
* Publish to `npm` with a fixed package.json.
|
||||||
|
* Conversion to beautiful TypeScript 1.4 code.
|
||||||
|
|
||||||
### Pending Implementation
|
### Pending Implementation
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
require(path.join(path.dirname(fs.realpathSync(__filename)), '..'));
|
require(path.join(path.dirname(fs.realpathSync(__filename)), '../dist/cli'));
|
||||||
102
crunchyroll.js.njsproj
Normal file
102
crunchyroll.js.njsproj
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<EnableTypeScript>True</EnableTypeScript>
|
||||||
|
<OutputPath>.</OutputPath>
|
||||||
|
<ProjectGuid>{c5cff68a-d733-4347-83e7-6e5fe58eb0e3}</ProjectGuid>
|
||||||
|
<ProjectHome />
|
||||||
|
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
|
||||||
|
<ProjectView>ShowAllFiles</ProjectView>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<StartupFile>cli.ts</StartupFile>
|
||||||
|
<TypeScriptModuleKind>CommonJS</TypeScriptModuleKind>
|
||||||
|
<TypeScriptNoImplicitAny>True</TypeScriptNoImplicitAny>
|
||||||
|
<TypeScriptOutDir>dist</TypeScriptOutDir>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
<WorkingDirectory>.</WorkingDirectory>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<TypeScriptSourceMap>True</TypeScriptSourceMap>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<TypeScriptGeneratesDeclarations>True</TypeScriptGeneratesDeclarations>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<TypeScriptCompile Include="src\cli.ts" />
|
||||||
|
<TypeScriptCompile Include="src\batch.ts" />
|
||||||
|
<TypeScriptCompile Include="src\episode.ts" />
|
||||||
|
<TypeScriptCompile Include="src\index.ts" />
|
||||||
|
<TypeScriptCompile Include="src\request.ts" />
|
||||||
|
<TypeScriptCompile Include="src\series.ts" />
|
||||||
|
<TypeScriptCompile Include="src\subtitle\decode.ts" />
|
||||||
|
<TypeScriptCompile Include="src\subtitle\formats\ass.ts" />
|
||||||
|
<TypeScriptCompile Include="src\subtitle\formats\index.ts" />
|
||||||
|
<TypeScriptCompile Include="src\subtitle\formats\srt.ts" />
|
||||||
|
<TypeScriptCompile Include="src\subtitle\index.ts" />
|
||||||
|
<TypeScriptCompile Include="src\typings.ts" />
|
||||||
|
<TypeScriptCompile Include="src\video\index.ts" />
|
||||||
|
<TypeScriptCompile Include="src\video\merge.ts" />
|
||||||
|
<TypeScriptCompile Include="src\video\stream.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\big-integer\big-integer.d.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\cheerio\cheerio.d.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\commander\commander.d.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\form-data\form-data.d.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\mkdirp\mkdirp.d.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\node\node.d.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\request\request.d.ts" />
|
||||||
|
<TypeScriptCompile Include="typings\xml2js\xml2js.d.ts" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="src\" />
|
||||||
|
<Folder Include="src\subtitle\" />
|
||||||
|
<Folder Include="src\subtitle\formats\" />
|
||||||
|
<Folder Include="src\video\" />
|
||||||
|
<Folder Include="typings" />
|
||||||
|
<Folder Include="typings\big-integer\" />
|
||||||
|
<Folder Include="typings\cheerio\" />
|
||||||
|
<Folder Include="typings\commander\" />
|
||||||
|
<Folder Include="typings\form-data\" />
|
||||||
|
<Folder Include="typings\mkdirp\" />
|
||||||
|
<Folder Include="typings\node" />
|
||||||
|
<Folder Include="typings\request\" />
|
||||||
|
<Folder Include="typings\xml2js\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="False" />
|
||||||
|
<Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsTools.targets" />
|
||||||
|
<ProjectExtensions>
|
||||||
|
<VisualStudio>
|
||||||
|
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||||
|
<WebProjectProperties>
|
||||||
|
<AutoAssignPort>True</AutoAssignPort>
|
||||||
|
<CustomServerUrl>http://localhost:1337</CustomServerUrl>
|
||||||
|
<DevelopmentServerPort>0</DevelopmentServerPort>
|
||||||
|
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||||
|
<IISUrl>http://localhost:48022/</IISUrl>
|
||||||
|
<NTLMAuthentication>False</NTLMAuthentication>
|
||||||
|
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||||
|
<UseCustomServer>True</UseCustomServer>
|
||||||
|
<UseIIS>False</UseIIS>
|
||||||
|
</WebProjectProperties>
|
||||||
|
</FlavorProperties>
|
||||||
|
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
|
||||||
|
<WebProjectProperties>
|
||||||
|
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
|
||||||
|
<AspNetDebugging>True</AspNetDebugging>
|
||||||
|
<EnableENC>False</EnableENC>
|
||||||
|
<ExternalProgram />
|
||||||
|
<NativeDebugging>False</NativeDebugging>
|
||||||
|
<SilverlightDebugging>False</SilverlightDebugging>
|
||||||
|
<SQLDebugging>False</SQLDebugging>
|
||||||
|
<StartAction>CurrentPage</StartAction>
|
||||||
|
<StartCmdLineArguments />
|
||||||
|
<StartExternalURL />
|
||||||
|
<StartPageUrl />
|
||||||
|
<StartWorkingDirectory />
|
||||||
|
</WebProjectProperties>
|
||||||
|
</FlavorProperties>
|
||||||
|
</VisualStudio>
|
||||||
|
</ProjectExtensions>
|
||||||
|
</Project>
|
||||||
22
crunchyroll.js.sln
Normal file
22
crunchyroll.js.sln
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 2013
|
||||||
|
VisualStudioVersion = 12.0.31101.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "crunchyroll.js", "crunchyroll.js.njsproj", "{C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C5CFF68A-D733-4347-83E7-6E5FE58EB0E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
57
crunchyroll.js.sln.DotSettings
Normal file
57
crunchyroll.js.sln.DotSettings
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DeclarationHides/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAllPathsReturnValue/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantQualifier/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SpecifyVariableTypeExplicitly/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/TypeScriptInspections/Level/@EntryValue">TypeScript14</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=TypeScript/@EntryIndexedValue"><?xml version="1.0" encoding="utf-16"?><Profile name="TypeScript"><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><JsInsertSemicolon>True</JsInsertSemicolon><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs></Profile></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue">TypeScript</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/FORCE_CONTROL_STATEMENTS_BRACES/@EntryValue">ONLY_FOR_MULTILINE</s:String>
|
||||||
|
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/KEEP_BLANK_LINES_BETWEEN_DECLARATIONS/@EntryValue">1</s:Int64>
|
||||||
|
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/KEEP_BLANK_LINES_IN_CODE/@EntryValue">1</s:Int64>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/QUOTE_STYLE/@EntryValue">SingleQuoted</s:String>
|
||||||
|
|
||||||
|
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/SPACE_WITHIN_OBJECT_LITERAL_BRACES/@EntryValue">False</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/STICK_COMMENT/@EntryValue">False</s:Boolean>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FCONSTANT/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FCONSTRUCTOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FFUNCTION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FGLOBAL_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLABEL/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLOCAL_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FOBJECT_005FPROPERTY_005FOF_005FFUNCTION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FPARAMETER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FCLASS/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM_005FMEMBER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FINTERFACE/@EntryIndexedValue"><Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FINTERFACE_005FFOR_005FJS_005FGLOBAL_005FVARIABLE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FEXPORTED/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FLOCAL/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FMEMBER_005FACCESSOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FMETHOD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FMEMBER_005FACCESSOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FMETHOD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FMEMBER_005FACCESSOR/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FMETHOD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FTYPE_005FPARAMETER/@EntryIndexedValue"><Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /></s:String>
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/TypeScriptCodeStyle/ExplicitPublicModifier/@EntryValue">True</s:Boolean>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/TypeScriptCodeStyle/FileReferenceStyle/@EntryValue">RelativeDotSlash</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/TypeScriptCodeStyle/NoImplicitAny/@EntryValue">True</s:Boolean>
|
||||||
|
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/TypeScriptCodeStyle/PreferUsingAliases/@EntryValue">False</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=AAA631615CEE9646AA8766F222F9457C/@KeyIndexDefined">True</s:Boolean>
|
||||||
|
<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=AAA631615CEE9646AA8766F222F9457C/AbsolutePath/@EntryValue">C:\Dropbox\Github\crunchyroll.js\crunchyroll.js.sln.DotSettings</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileAAA631615CEE9646AA8766F222F9457C/@KeyIndexDefined">True</s:Boolean>
|
||||||
|
<s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileAAA631615CEE9646AA8766F222F9457C/RelativePriority/@EntryValue">1</s:Double></wpf:ResourceDictionary>
|
||||||
6
index.js
6
index.js
@ -1,6 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var src = require('./src');
|
|
||||||
|
|
||||||
src.batch(process.argv, function(err) {
|
|
||||||
if (err) return console.error(err.stack || err);
|
|
||||||
});
|
|
||||||
14
package.json
14
package.json
@ -8,19 +8,25 @@
|
|||||||
],
|
],
|
||||||
"name": "crunchyroll",
|
"name": "crunchyroll",
|
||||||
"repository": "git://github.com/Deathspike/crunchyroll.js.git",
|
"repository": "git://github.com/Deathspike/crunchyroll.js.git",
|
||||||
"version": "1.0.6",
|
"version": "1.1.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"crunchyroll": "./bin/crunchyroll"
|
"crunchyroll": "./bin/crunchyroll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"big-integer": "^1.4.1",
|
"big-integer": "^1.4.3",
|
||||||
"cheerio": "^0.18.0",
|
"cheerio": "^0.18.0",
|
||||||
"commander": "^2.6.0",
|
"commander": "^2.6.0",
|
||||||
"mkdirp": "^0.5.0",
|
"mkdirp": "^0.5.0",
|
||||||
"request": "^2.51.0",
|
"request": "^2.53.0",
|
||||||
"xml2js": "^0.4.4"
|
"xml2js": "^0.4.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"prepublish": "npm run tsd && npm run tsc",
|
||||||
|
"tsc": "./node_modules/.bin/tsc --declaration --module CommonJS --noImplicitAny --outDir dist typings/tsd.d.ts src/cli.ts",
|
||||||
|
"tsd": "./node_modules/.bin/tsd reinstall --overwrite --save"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsd": "^0.5.7",
|
||||||
|
"typescript": "^1.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +1,41 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
var Command = require('commander').Command;
|
export = main;
|
||||||
var fs = require('fs');
|
import commander = require('commander');
|
||||||
var path = require('path');
|
import fs = require('fs');
|
||||||
var series = require('./series');
|
import path = require('path');
|
||||||
|
import series = require('./series');
|
||||||
|
import typings = require('./typings');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the batch of series to disk.
|
* Streams the batch of series to disk.
|
||||||
* @param {Array.<string>} args
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
*/
|
||||||
module.exports = function(args, done) {
|
function main(args: string[], done: (err?: Error) => void) {
|
||||||
var config = _parse(args);
|
var config = parse(args);
|
||||||
var batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
|
var batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
|
||||||
_tasks(config, batchPath, function(err, tasks) {
|
tasks(config, batchPath, (err, tasks) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var i = 0;
|
var i = 0;
|
||||||
(function next() {
|
(function next() {
|
||||||
if (i >= tasks.length) return done();
|
if (i >= tasks.length) return done();
|
||||||
series(tasks[i].config, tasks[i].address, function(err) {
|
series(tasks[i].config, tasks[i].address, err => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
i += 1;
|
i += 1;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits the value into arguments.
|
* Splits the value into arguments.
|
||||||
* @private
|
|
||||||
* @param {string} value
|
|
||||||
* @returns {Array.<string>}
|
|
||||||
*/
|
*/
|
||||||
function _split(value) {
|
function split(value: string): string[] {
|
||||||
var inQuote = false;
|
var inQuote = false;
|
||||||
var pieces = [];
|
var i: number;
|
||||||
|
var pieces: string[] = [];
|
||||||
var previous = 0;
|
var previous = 0;
|
||||||
for (var i = 0; i < value.length; i += 1) {
|
for (i = 0; i < value.length; i += 1) {
|
||||||
if (value.charAt(i) === '"') {
|
if (value.charAt(i) === '"') inQuote = !inQuote;
|
||||||
inQuote = !inQuote;
|
|
||||||
}
|
|
||||||
if (!inQuote && value.charAt(i) === ' ') {
|
if (!inQuote && value.charAt(i) === ' ') {
|
||||||
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]);
|
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]);
|
||||||
previous = i + 1;
|
previous = i + 1;
|
||||||
@ -51,43 +47,36 @@ function _split(value) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the configuration or reads the batch-mode file for tasks.
|
* Parses the configuration or reads the batch-mode file for tasks.
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} batchPath
|
|
||||||
* @param {function(Error, Object=} done
|
|
||||||
*/
|
*/
|
||||||
function _tasks(config, batchPath, done) {
|
function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error, tasks?: typings.IConfigTask[]) => void) {
|
||||||
if (config.args.length) {
|
if (config.args.length) {
|
||||||
return done(undefined, config.args.map(function(address) {
|
return done(null, config.args.map(address => {
|
||||||
return {address: address, config: config};
|
return {address: address, config: config};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
fs.exists(batchPath, function(exists) {
|
fs.exists(batchPath, exists => {
|
||||||
if (!exists) return done(undefined, []);
|
if (!exists) return done(null, []);
|
||||||
fs.readFile(batchPath, 'utf8', function(err, data) {
|
fs.readFile(batchPath, 'utf8', (err, data) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var map = [];
|
var map: typings.IConfigTask[] = [];
|
||||||
data.split(/\r?\n/).forEach(function(line) {
|
data.split(/\r?\n/).forEach(line => {
|
||||||
if (/^(\/\/|#)/.test(line)) return;
|
if (/^(\/\/|#)/.test(line)) return;
|
||||||
var lineConfig = _parse(process.argv.concat(_split(line)));
|
var lineConfig = parse(process.argv.concat(split(line)));
|
||||||
lineConfig.args.forEach(function(address) {
|
lineConfig.args.forEach(address => {
|
||||||
if (!address) return;
|
if (!address) return;
|
||||||
map.push({address: address, config: lineConfig});
|
map.push({address: address, config: lineConfig});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
done(undefined, map);
|
done(null, map);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the arguments and returns a configuration.
|
* Parses the arguments and returns a configuration.
|
||||||
* @private
|
|
||||||
* @param {Array.<string>} args
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
*/
|
||||||
function _parse(args) {
|
function parse(args: string[]): typings.IConfigLine {
|
||||||
return new Command().version(require('../package').version)
|
return new commander.Command().version(require('../package').version)
|
||||||
// Authentication
|
// Authentication
|
||||||
.option('-p, --pass <s>', 'The password.')
|
.option('-p, --pass <s>', 'The password.')
|
||||||
.option('-u, --user <s>', 'The e-mail address or username.')
|
.option('-u, --user <s>', 'The e-mail address or username.')
|
||||||
6
src/cli.ts
Normal file
6
src/cli.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
'use strict';
|
||||||
|
import batch = require('./batch');
|
||||||
|
|
||||||
|
batch(process.argv, err => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
});
|
||||||
204
src/episode.js
204
src/episode.js
@ -1,204 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var cheerio = require('cheerio');
|
|
||||||
var fs = require('fs');
|
|
||||||
var mkdirp = require('mkdirp');
|
|
||||||
var request = require('./request');
|
|
||||||
var path = require('path');
|
|
||||||
var subtitle = require('./subtitle');
|
|
||||||
var video = require('./video');
|
|
||||||
var xml2js = require('xml2js');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Streams the episode to disk.
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} address
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
module.exports = function (config, address, done) {
|
|
||||||
_page(config, address, function(err, page) {
|
|
||||||
if (err) return done(err);
|
|
||||||
_player(config, address, page.id, function(err, player) {
|
|
||||||
if (err) return done(err);
|
|
||||||
_download(config, page, player, done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Completes a download and writes the message with an elapsed time.
|
|
||||||
* @private
|
|
||||||
* @param {string} message
|
|
||||||
* @param {number} begin
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function _complete(message, begin, done) {
|
|
||||||
var timeInMs = Date.now() - begin;
|
|
||||||
var seconds = _prefix(Math.floor(timeInMs / 1000) % 60, 2);
|
|
||||||
var minutes = _prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
|
|
||||||
var hours = _prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
|
|
||||||
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads the subtitle and video.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {Object} page
|
|
||||||
* @param {Object} player
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function _download(config, page, player, done) {
|
|
||||||
var series = config.series || page.series;
|
|
||||||
var fileName = _name(config, page, series);
|
|
||||||
var filePath = path.join(config.output || process.cwd(), series, fileName);
|
|
||||||
mkdirp(path.dirname(filePath), function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
_subtitle(config, player, filePath, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
var now = Date.now();
|
|
||||||
console.log('Fetching ' + fileName);
|
|
||||||
_video(config, page, player, filePath, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
if (config.merge) return _complete('Finished ' + fileName, now, done);
|
|
||||||
video.merge(config, player.video.file, filePath, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
_complete('Finished ' + fileName, now, done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Names the file based on the config, page, series and tag.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {Object} page
|
|
||||||
* @param {string} series
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _name(config, page, series) {
|
|
||||||
var episode = (page.episode < 10 ? '0' : '') + page.episode;
|
|
||||||
var volume = (page.volume < 10 ? '0' : '') + page.volume;
|
|
||||||
var tag = config.tag || 'CrunchyRoll';
|
|
||||||
return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests the page data and scrapes the id, episode, series and swf.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} address
|
|
||||||
* @param {function(Error, Object=)} done
|
|
||||||
*/
|
|
||||||
function _page(config, address, done) {
|
|
||||||
var id = parseInt((address.match(/[0-9]+$/) || [0])[0], 10);
|
|
||||||
if (!id) return done(new Error('Invalid address.'));
|
|
||||||
request.get(config, address, function(err, res, body) {
|
|
||||||
if (err) return done(err);
|
|
||||||
var $ = cheerio.load(body);
|
|
||||||
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
|
|
||||||
var regexp = /Watch\s+(.+?)(?:\s+Season\s+([0-9]+))?\s+Episode\s+([0-9]+)/;
|
|
||||||
var data = regexp.exec($('title').text());
|
|
||||||
if (!swf || !data) return done(new Error('Invalid page.'));
|
|
||||||
done(undefined, {
|
|
||||||
id: id,
|
|
||||||
episode: parseInt(data[3], 10),
|
|
||||||
series: data[1],
|
|
||||||
swf: swf[1],
|
|
||||||
volume: parseInt(data[2], 10) || 1
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefixes a value.
|
|
||||||
* @private
|
|
||||||
* @param {(number|string)} value
|
|
||||||
* @param {number} length
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _prefix(value, length) {
|
|
||||||
if (typeof value !== 'string') value = String(value);
|
|
||||||
while (value.length < length) value = '0' + value;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests the player data and scrapes the subtitle and video data.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} address
|
|
||||||
* @param {number} id
|
|
||||||
* @param {function(Error, Object=)} done
|
|
||||||
*/
|
|
||||||
function _player(config, address, id, done) {
|
|
||||||
var url = address.match(/^(https?:\/\/[^\/]+)/);
|
|
||||||
if (!url) return done(new Error('Invalid address.'));
|
|
||||||
request.post(config, {
|
|
||||||
form: {current_page: address},
|
|
||||||
url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id
|
|
||||||
}, function(err, res, xml) {
|
|
||||||
if (err) return done(err);
|
|
||||||
xml2js.parseString(xml, {
|
|
||||||
explicitArray: false,
|
|
||||||
explicitRoot: false
|
|
||||||
}, function(err, player) {
|
|
||||||
if (err) return done(err);
|
|
||||||
try {
|
|
||||||
done(undefined, {
|
|
||||||
subtitle: {
|
|
||||||
id: player['default:preload'].subtitle.$.id,
|
|
||||||
iv: player['default:preload'].subtitle.iv,
|
|
||||||
data: player['default:preload'].subtitle.data
|
|
||||||
},
|
|
||||||
video: {
|
|
||||||
file: player['default:preload'].stream_info.file,
|
|
||||||
host: player['default:preload'].stream_info.host
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch(err) {
|
|
||||||
done(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the subtitles to disk.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {Object} player
|
|
||||||
* @param {string} filePath
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function _subtitle(config, player, filePath, done) {
|
|
||||||
var enc = player.subtitle;
|
|
||||||
subtitle.decode(enc.id, enc.iv, enc.data, function(err, data) {
|
|
||||||
if (err) return done(err);
|
|
||||||
var format = subtitle.formats[config.format] ? config.format : 'ass';
|
|
||||||
subtitle.formats[format](data, function(err, decodedSubtitle) {
|
|
||||||
if (err) return done(err);
|
|
||||||
fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Streams the video to disk.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {Object} page
|
|
||||||
* @param {Object} player
|
|
||||||
* @param {string} filePath
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function _video(config, page, player, filePath, done) {
|
|
||||||
video.stream(
|
|
||||||
player.video.host,
|
|
||||||
player.video.file,
|
|
||||||
page.swf,
|
|
||||||
filePath + path.extname(player.video.file),
|
|
||||||
done);
|
|
||||||
}
|
|
||||||
166
src/episode.ts
Normal file
166
src/episode.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
'use strict';
|
||||||
|
export = main;
|
||||||
|
import cheerio = require('cheerio');
|
||||||
|
import fs = require('fs');
|
||||||
|
import mkdirp = require('mkdirp');
|
||||||
|
import request = require('./request');
|
||||||
|
import path = require('path');
|
||||||
|
import subtitle = require('./subtitle/index');
|
||||||
|
import typings = require('./typings');
|
||||||
|
import video = require('./video/index');
|
||||||
|
import xml2js = require('xml2js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the episode to disk.
|
||||||
|
*/
|
||||||
|
function main(config: typings.IConfig, address: string, done: (err: Error) => void) {
|
||||||
|
scrapePage(config, address, (err, page) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
scrapePlayer(config, address, page.id, (err, player) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
download(config, page, player, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completes a download and writes the message with an elapsed time.
|
||||||
|
*/
|
||||||
|
function complete(message: string, begin: number, done: (err: Error) => void) {
|
||||||
|
var timeInMs = Date.now() - begin;
|
||||||
|
var seconds = prefix(Math.floor(timeInMs / 1000) % 60, 2);
|
||||||
|
var minutes = prefix(Math.floor(timeInMs / 1000 / 60) % 60, 2);
|
||||||
|
var hours = prefix(Math.floor(timeInMs / 1000 / 60 / 60), 2);
|
||||||
|
console.log(message + ' (' + hours + ':' + minutes + ':' + seconds + ')');
|
||||||
|
done(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the subtitle and video.
|
||||||
|
*/
|
||||||
|
function download(config: typings.IConfig, page: typings.IEpisodePage, player: typings.IEpisodePlayer, done: (err: Error) => void) {
|
||||||
|
var series = config.series || page.series;
|
||||||
|
var fileName = name(config, page, series);
|
||||||
|
var filePath = path.join(config.output || process.cwd(), series, fileName);
|
||||||
|
mkdirp(path.dirname(filePath), (err: Error) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
downloadSubtitle(config, player, filePath, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
var now = Date.now();
|
||||||
|
console.log('Fetching ' + fileName);
|
||||||
|
downloadVideo(config, page, player, filePath, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
if (config.merge) return complete('Finished ' + fileName, now, done);
|
||||||
|
video.merge(config, player.video.file, filePath, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
complete('Finished ' + fileName, now, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the subtitles to disk.
|
||||||
|
*/
|
||||||
|
function downloadSubtitle(config: typings.IConfig, player: typings.IEpisodePlayer, filePath: string, done: (err: Error) => void) {
|
||||||
|
var enc = player.subtitle;
|
||||||
|
subtitle.decode(enc.id, enc.iv, enc.data, (err, data) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
var formats = subtitle.formats;
|
||||||
|
var format = formats[config.format] ? config.format : 'ass';
|
||||||
|
formats[format](data, (err: Error, decodedSubtitle: string) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
fs.writeFile(filePath + '.' + format, '\ufeff' + decodedSubtitle, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the video to disk.
|
||||||
|
*/
|
||||||
|
function downloadVideo(config: typings.IConfig, page: typings.IEpisodePage, player: typings.IEpisodePlayer, filePath: string, done: (err: Error) => void) {
|
||||||
|
video.stream(
|
||||||
|
player.video.host,
|
||||||
|
player.video.file,
|
||||||
|
page.swf,
|
||||||
|
filePath + path.extname(player.video.file),
|
||||||
|
done);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Names the file based on the config, page, series and tag.
|
||||||
|
*/
|
||||||
|
function name(config: typings.IConfig, page: typings.IEpisodePage, series: string) {
|
||||||
|
var episode = (page.episode < 10 ? '0' : '') + page.episode;
|
||||||
|
var volume = (page.volume < 10 ? '0' : '') + page.volume;
|
||||||
|
var tag = config.tag || 'CrunchyRoll';
|
||||||
|
return series + ' ' + volume + 'x' + episode + ' [' + tag + ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefixes a value.
|
||||||
|
*/
|
||||||
|
function prefix(value: number|string, length: number) {
|
||||||
|
var valueString = typeof value !== 'string' ? String(value) : value;
|
||||||
|
while (valueString.length < length) valueString = '0' + valueString;
|
||||||
|
return valueString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the page data and scrapes the id, episode, series and swf.
|
||||||
|
*/
|
||||||
|
function scrapePage(config: typings.IConfig, address: string, done: (err: Error, page?: typings.IEpisodePage) => void) {
|
||||||
|
var id = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
|
||||||
|
if (!id) return done(new Error('Invalid address.'));
|
||||||
|
request.get(config, address, (err, result) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
var $ = cheerio.load(result);
|
||||||
|
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
|
||||||
|
var regexp = /Watch\s+(.+?)(?:\s+Season\s+([0-9]+))?\s+Episode\s+([0-9]+)/;
|
||||||
|
var data = regexp.exec($('title').text());
|
||||||
|
if (!swf || !data) return done(new Error('Invalid page.'));
|
||||||
|
done(null, {
|
||||||
|
id: id,
|
||||||
|
episode: parseInt(data[3], 10),
|
||||||
|
series: data[1],
|
||||||
|
swf: swf[1],
|
||||||
|
volume: parseInt(data[2], 10) || 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the player data and scrapes the subtitle and video data.
|
||||||
|
*/
|
||||||
|
function scrapePlayer(config: typings.IConfig, address: string, id: number, done: (err: Error, player?: typings.IEpisodePlayer) => void) {
|
||||||
|
var url = address.match(/^(https?:\/\/[^\/]+)/);
|
||||||
|
if (!url) return done(new Error('Invalid address.'));
|
||||||
|
request.post(config, {
|
||||||
|
form: {current_page: address},
|
||||||
|
url: url[1] + '/xml/?req=RpcApiVideoPlayer_GetStandardConfig&media_id=' + id
|
||||||
|
}, (err, result) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
xml2js.parseString(result, {
|
||||||
|
explicitArray: false,
|
||||||
|
explicitRoot: false
|
||||||
|
}, (err: Error, player: typings.IEpisodePlayerConfig) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
try {
|
||||||
|
done(null, {
|
||||||
|
subtitle: {
|
||||||
|
id: parseInt(player['default:preload'].subtitle.$.id, 10),
|
||||||
|
iv: player['default:preload'].subtitle.iv,
|
||||||
|
data: player['default:preload'].subtitle.data
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
file: player['default:preload'].stream_info.file,
|
||||||
|
host: player['default:preload'].stream_info.host
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (parseError) {
|
||||||
|
done(parseError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
batch: require('./batch'),
|
|
||||||
episode: require('./episode'),
|
|
||||||
series: require('./series')
|
|
||||||
};
|
|
||||||
4
src/index.ts
Normal file
4
src/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
'use strict';
|
||||||
|
export import batch = require('./batch');
|
||||||
|
export import episode = require('./episode');
|
||||||
|
export import series = require('./series');
|
||||||
@ -1,66 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var isAuthenticated = false;
|
|
||||||
var request = require('request');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a GET request for the resource.
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {(string|Object)} options
|
|
||||||
* @param {function(Error, Object, string)} done
|
|
||||||
*/
|
|
||||||
module.exports.get = function(config, options, done) {
|
|
||||||
_authenticate(config, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
request.get(_modify(options), done);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a POST request for the resource.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {(string|Object)} options
|
|
||||||
* @param {function(Error, Object, string)} done
|
|
||||||
*/
|
|
||||||
module.exports.post = function(config, options, done) {
|
|
||||||
_authenticate(config, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
request.post(_modify(options), done);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticates using the configured pass and user.
|
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function _authenticate(config, done) {
|
|
||||||
if (isAuthenticated || !config.pass || !config.user) return done();
|
|
||||||
request.post({
|
|
||||||
form: {
|
|
||||||
formname: 'RpcApiUser_Login',
|
|
||||||
fail_url: 'https://www.crunchyroll.com/login',
|
|
||||||
name: config.user,
|
|
||||||
password: config.pass
|
|
||||||
},
|
|
||||||
jar: true,
|
|
||||||
url: 'https://www.crunchyroll.com/?a=formhandler'
|
|
||||||
}, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
isAuthenticated = true;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modifies the options to use the authenticated cookie jar.
|
|
||||||
* @private
|
|
||||||
* @param {(string|Object)} options
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function _modify(options) {
|
|
||||||
if (typeof options === 'string') options = {url: options};
|
|
||||||
options.jar = true;
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
63
src/request.ts
Normal file
63
src/request.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
import request = require('request');
|
||||||
|
import typings = require('./typings');
|
||||||
|
var isAuthenticated = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a GET request for the resource.
|
||||||
|
*/
|
||||||
|
export function get(config: typings.IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
|
||||||
|
authenticate(config, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
request.get(modify(options), (err: Error, response: any, body: any) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
done(null, typeof body === 'string' ? body : String(body));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a POST request for the resource.
|
||||||
|
*/
|
||||||
|
export function post(config: typings.IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
|
||||||
|
authenticate(config, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
request.post(modify(options), (err: Error, response: any, body: any) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
done(null, typeof body === 'string' ? body : String(body));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates using the configured pass and user.
|
||||||
|
*/
|
||||||
|
function authenticate(config: typings.IConfig, done: (err: Error) => void) {
|
||||||
|
if (isAuthenticated || !config.pass || !config.user) return done(null);
|
||||||
|
var options = {
|
||||||
|
form: {
|
||||||
|
formname: 'RpcApiUser_Login',
|
||||||
|
fail_url: 'https://www.crunchyroll.com/login',
|
||||||
|
name: config.user,
|
||||||
|
password: config.pass
|
||||||
|
},
|
||||||
|
jar: true,
|
||||||
|
url: 'https://www.crunchyroll.com/?a=formhandler'
|
||||||
|
};
|
||||||
|
request.post(options, (err: Error) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
isAuthenticated = true;
|
||||||
|
done(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the options to use the authenticated cookie jar.
|
||||||
|
*/
|
||||||
|
function modify(options: string|request.Options): request.Options {
|
||||||
|
if (typeof options !== 'string') {
|
||||||
|
options.jar = true;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
return {jar: true, url: options.toString()};
|
||||||
|
}
|
||||||
@ -1,31 +1,30 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
var cheerio = require('cheerio');
|
export = main;
|
||||||
var episode = require('./episode');
|
import cheerio = require('cheerio');
|
||||||
|
import episode = require('./episode');
|
||||||
|
import fs = require('fs');
|
||||||
|
import request = require('./request');
|
||||||
|
import path = require('path');
|
||||||
|
import typings = require('./typings');
|
||||||
|
import url = require('url');
|
||||||
var persistent = '.crpersistent';
|
var persistent = '.crpersistent';
|
||||||
var fs = require('fs');
|
|
||||||
var request = require('./request');
|
|
||||||
var path = require('path');
|
|
||||||
var url = require('url');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the series to disk.
|
* Streams the series to disk.
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} address
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
*/
|
||||||
module.exports = function(config, address, done) {
|
function main(config: typings.IConfig, address: string, done: (err: Error) => void) {
|
||||||
var persistentPath = path.join(config.output || process.cwd(), persistent);
|
var persistentPath = path.join(config.output || process.cwd(), persistent);
|
||||||
fs.readFile(persistentPath, 'utf8', function(err, contents) {
|
fs.readFile(persistentPath, 'utf8', (err, contents) => {
|
||||||
var cache = config.cache ? {} : JSON.parse(contents || '{}');
|
var cache = config.cache ? {} : JSON.parse(contents || '{}');
|
||||||
_page(config, address, function(err, page) {
|
page(config, address, (err, page) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var i = 0;
|
var i = 0;
|
||||||
(function next() {
|
(function next() {
|
||||||
if (i >= page.episodes.length) return done();
|
if (i >= page.episodes.length) return done(null);
|
||||||
_download(cache, config, address, page.episodes[i], function(err) {
|
download(cache, config, address, page.episodes[i], err => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var newCache = JSON.stringify(cache, null, ' ');
|
var newCache = JSON.stringify(cache, null, ' ');
|
||||||
fs.writeFile(persistentPath, newCache, function(err) {
|
fs.writeFile(persistentPath, newCache, err => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
i += 1;
|
i += 1;
|
||||||
next();
|
next();
|
||||||
@ -34,42 +33,33 @@ module.exports = function(config, address, done) {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads the episode.
|
* Downloads the episode.
|
||||||
* @private
|
|
||||||
* @param {Object.<string, string>} cache
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} baseAddress
|
|
||||||
* @param {Object} item
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
*/
|
||||||
function _download(cache, config, baseAddress, item, done) {
|
function download(cache: {[address: string]: number}, config: typings.IConfig, baseAddress: string, item: typings.ISeriesEpisode, done: (err: Error) => void) {
|
||||||
if (!_filter(config, item)) return done();
|
if (!filter(config, item)) return done(null);
|
||||||
var address = url.resolve(baseAddress, item.address);
|
var address = url.resolve(baseAddress, item.address);
|
||||||
if (cache[address]) return done();
|
if (cache[address]) return done(null);
|
||||||
episode(config, address, function(err) {
|
episode(config, address, err => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
cache[address] = Date.now();
|
cache[address] = Date.now();
|
||||||
done();
|
done(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the item based on the configuration.
|
* Filters the item based on the configuration.
|
||||||
* @param {Object} config
|
|
||||||
* @param {Object} item
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
function _filter(config, item) {
|
function filter(config: typings.IConfig, item: typings.ISeriesEpisode) {
|
||||||
// Filter on chapter.
|
// Filter on chapter.
|
||||||
var episodeFilter = parseInt(config.episode, 10);
|
var episodeFilter = config.episode;
|
||||||
if (episodeFilter > 0 && item.episode <= episodeFilter) return false;
|
if (episodeFilter > 0 && item.episode <= episodeFilter) return false;
|
||||||
if (episodeFilter < 0 && item.episode >= -episodeFilter) return false;
|
if (episodeFilter < 0 && item.episode >= -episodeFilter) return false;
|
||||||
|
|
||||||
// Filter on volume.
|
// Filter on volume.
|
||||||
var volumeFilter = parseInt(config.volume, 10);
|
var volumeFilter = config.volume;
|
||||||
if (volumeFilter > 0 && item.volume <= volumeFilter) return false;
|
if (volumeFilter > 0 && item.volume <= volumeFilter) return false;
|
||||||
if (volumeFilter < 0 && item.volume >= -volumeFilter) return false;
|
if (volumeFilter < 0 && item.volume >= -volumeFilter) return false;
|
||||||
return true;
|
return true;
|
||||||
@ -77,19 +67,15 @@ function _filter(config, item) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the page and scrapes the episodes and series.
|
* Requests the page and scrapes the episodes and series.
|
||||||
* @private
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} address
|
|
||||||
* @param {function(Error, Object=)} done
|
|
||||||
*/
|
*/
|
||||||
function _page(config, address, done) {
|
function page(config: typings.IConfig, address: string, done: (err: Error, result?: typings.ISeries) => void) {
|
||||||
request.get(config, address, function(err, res, body) {
|
request.get(config, address, (err, result) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
var $ = cheerio.load(body);
|
var $ = cheerio.load(result);
|
||||||
var title = $('span[itemprop=name]').text();
|
var title = $('span[itemprop=name]').text();
|
||||||
if (!title) return done(new Error('Invalid page.'));
|
if (!title) return done(new Error('Invalid page.'));
|
||||||
var episodes = [];
|
var episodes: typings.ISeriesEpisode[] = [];
|
||||||
$('.episode').each(function(i, el) {
|
$('.episode').each((i, el) => {
|
||||||
if ($(el).children('img[src*=coming_soon]').length) return;
|
if ($(el).children('img[src*=coming_soon]').length) return;
|
||||||
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
|
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
|
||||||
var regexp = /Episode\s+([0-9]+)\s*$/i;
|
var regexp = /Episode\s+([0-9]+)\s*$/i;
|
||||||
@ -102,6 +88,6 @@ function _page(config, address, done) {
|
|||||||
volume: volume ? parseInt(volume[0], 10) : 1
|
volume: volume ? parseInt(volume[0], 10) : 1
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
done(undefined, {episodes: episodes.reverse(), series: title});
|
done(null, {episodes: episodes.reverse(), series: title});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1,98 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var crypto = require('crypto');
|
|
||||||
var bigInt = require('big-integer');
|
|
||||||
var zlib = require('zlib');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the data.
|
|
||||||
* @param {number} id
|
|
||||||
* @param {(Buffer|string)} iv
|
|
||||||
* @param {(Buffer|string)} data
|
|
||||||
* @param {function(Error, Buffer=)} done
|
|
||||||
*/
|
|
||||||
module.exports = function(id, iv, data, done) {
|
|
||||||
try {
|
|
||||||
_decompress(_decrypt(id, iv, data), done);
|
|
||||||
} catch(e) {
|
|
||||||
done(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypts the data.
|
|
||||||
* @private
|
|
||||||
* @param {number} id
|
|
||||||
* @param {(Buffer|string)} iv
|
|
||||||
* @param {(Buffer|string)} data
|
|
||||||
* @return {Buffer}
|
|
||||||
*/
|
|
||||||
function _decrypt(id, iv, data) {
|
|
||||||
if (typeof iv === 'string') iv = new Buffer(iv, 'base64');
|
|
||||||
if (typeof data === 'string') data = new Buffer(data, 'base64');
|
|
||||||
var decipher = crypto.createDecipheriv('aes-256-cbc', _key(id), iv);
|
|
||||||
decipher.setAutoPadding(false);
|
|
||||||
return Buffer.concat([decipher.update(data), decipher.final()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decompresses the data.
|
|
||||||
* @private
|
|
||||||
* @param {Buffer} data
|
|
||||||
* @param {function(Error, Buffer=)} done
|
|
||||||
*/
|
|
||||||
function _decompress(data, done) {
|
|
||||||
try {
|
|
||||||
zlib.inflate(data, done);
|
|
||||||
} catch(e) {
|
|
||||||
done(undefined, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a key.
|
|
||||||
* @private
|
|
||||||
* @param {number} subtitleId
|
|
||||||
* @return {Buffer}
|
|
||||||
*/
|
|
||||||
function _key(subtitleId) {
|
|
||||||
var hash = _secret(20, 97, 1, 2) + _magic(subtitleId);
|
|
||||||
var result = new Buffer(32);
|
|
||||||
result.fill(0);
|
|
||||||
crypto.createHash('sha1').update(hash).digest().copy(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a magic number.
|
|
||||||
* @private
|
|
||||||
* @param {number} subtitleId
|
|
||||||
* @return {number}
|
|
||||||
*/
|
|
||||||
function _magic(subtitleId) {
|
|
||||||
var base = Math.floor(Math.sqrt(6.9) * Math.pow(2, 25));
|
|
||||||
var hash = bigInt(base).xor(subtitleId);
|
|
||||||
var multipliedHash = bigInt(hash).multiply(32);
|
|
||||||
return bigInt(hash).xor(hash >> 3).xor(multipliedHash).toJSNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a secret string based on a Fibonacci sequence.
|
|
||||||
* @private
|
|
||||||
* @param {number} size
|
|
||||||
* @param {number} modulo
|
|
||||||
* @param {number} firstSeed
|
|
||||||
* @param {number} secondSeed
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
function _secret(size, modulo, firstSeed, secondSeed) {
|
|
||||||
var currentValue = firstSeed + secondSeed;
|
|
||||||
var previousValue = secondSeed;
|
|
||||||
var result = '';
|
|
||||||
for (var i = 0; i < size; i += 1) {
|
|
||||||
var oldValue = currentValue;
|
|
||||||
result += String.fromCharCode(currentValue % modulo + 33);
|
|
||||||
currentValue += previousValue;
|
|
||||||
previousValue = oldValue;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
75
src/subtitle/decode.ts
Normal file
75
src/subtitle/decode.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
'use strict';
|
||||||
|
export = main;
|
||||||
|
import crypto = require('crypto');
|
||||||
|
import bigInt = require('big-integer');
|
||||||
|
import zlib = require('zlib');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the data.
|
||||||
|
*/
|
||||||
|
function main(id: number, iv: Buffer|string, data: Buffer|string, done: (err?: Error, result?: Buffer) => void) {
|
||||||
|
try {
|
||||||
|
decompress(decrypt(id, iv, data), done);
|
||||||
|
} catch (e) {
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the data.
|
||||||
|
*/
|
||||||
|
function decrypt(id: number, iv: Buffer|string, data: Buffer|string) {
|
||||||
|
var ivBuffer = typeof iv === 'string' ? new Buffer(iv, 'base64') : iv;
|
||||||
|
var dataBuffer = typeof data === 'string' ? new Buffer(data, 'base64') : data;
|
||||||
|
var decipher = crypto.createDecipheriv('aes-256-cbc', key(id), ivBuffer);
|
||||||
|
decipher.setAutoPadding(false);
|
||||||
|
return Buffer.concat([decipher.update(dataBuffer), decipher.final()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompresses the data.
|
||||||
|
*/
|
||||||
|
function decompress(data: Buffer, done: (err: Error, result?: Buffer) => void) {
|
||||||
|
try {
|
||||||
|
zlib.inflate(data, done);
|
||||||
|
} catch (e) {
|
||||||
|
done(null, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a key.
|
||||||
|
*/
|
||||||
|
function key(subtitleId: number): Buffer {
|
||||||
|
var hash = secret(20, 97, 1, 2) + magic(subtitleId);
|
||||||
|
var result = new Buffer(32);
|
||||||
|
result.fill(0);
|
||||||
|
crypto.createHash('sha1').update(hash).digest().copy(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a magic number.
|
||||||
|
*/
|
||||||
|
function magic(subtitleId: number): number {
|
||||||
|
var base = Math.floor(Math.sqrt(6.9) * Math.pow(2, 25));
|
||||||
|
var hash = bigInt(base).xor(subtitleId).toJSNumber();
|
||||||
|
var multipliedHash = bigInt(hash).multiply(32).toJSNumber();
|
||||||
|
return bigInt(hash).xor(hash >> 3).xor(multipliedHash).toJSNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a secret string based on a Fibonacci sequence.
|
||||||
|
*/
|
||||||
|
function secret(size: number, modulo: number, firstSeed: number, secondSeed: number): string {
|
||||||
|
var currentValue = firstSeed + secondSeed;
|
||||||
|
var previousValue = secondSeed;
|
||||||
|
var result = '';
|
||||||
|
for (var i = 0; i < size; i += 1) {
|
||||||
|
var oldValue = currentValue;
|
||||||
|
result += String.fromCharCode(currentValue % modulo + 33);
|
||||||
|
currentValue += previousValue;
|
||||||
|
previousValue = oldValue;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -1,92 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var xml2js = require('xml2js');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an input buffer to a SubStation Alpha subtitle.
|
|
||||||
* @param {Buffer|string} input
|
|
||||||
* @param {function(Error, string=)} done
|
|
||||||
*/
|
|
||||||
module.exports = function(input, done) {
|
|
||||||
if (typeof buffer !== 'string') input = input.toString();
|
|
||||||
xml2js.parseString(input, {
|
|
||||||
explicitArray: false,
|
|
||||||
explicitRoot: false
|
|
||||||
}, function(err, xml) {
|
|
||||||
if (err) return done(err);
|
|
||||||
try {
|
|
||||||
done(undefined, _script(xml) + '\n' +
|
|
||||||
_style(xml.styles) + '\n' +
|
|
||||||
_event(xml.events));
|
|
||||||
} catch(err) {
|
|
||||||
done(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the event block.
|
|
||||||
* @param {Object} events
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _event(events) {
|
|
||||||
var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
|
||||||
var items = [].concat(events.event).map(function(style) {
|
|
||||||
return _values(style.$, 'Dialogue: 0,{start},{end},{style},{name},' +
|
|
||||||
'{margin_l},{margin_r},{margin_v},{effect},{text}');
|
|
||||||
});
|
|
||||||
return '[Events]\n' +
|
|
||||||
'Format: ' + format + '\n' +
|
|
||||||
items.join('\n') + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the script block.
|
|
||||||
* @param {Object} script
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _script(script) {
|
|
||||||
return _values(script.$,
|
|
||||||
'[Script Info]\n' +
|
|
||||||
'Title: {title}\n' +
|
|
||||||
'ScriptType: v4.00+\n' +
|
|
||||||
'WrapStyle: {wrap_style}\n' +
|
|
||||||
'PlayResX: {play_res_x}}\n' +
|
|
||||||
'PlayResY: {play_res_y}\n' +
|
|
||||||
'Subtitle ID: {id}\n' +
|
|
||||||
'Language: {lang_string}\n' +
|
|
||||||
'Created: {created}\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the style block.
|
|
||||||
* @param {Object} styles
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _style(styles) {
|
|
||||||
var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
|
|
||||||
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
|
|
||||||
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
|
|
||||||
'MarginL,MarginR,MarginV,Encoding';
|
|
||||||
var items = [].concat(styles.style).map(function(style) {
|
|
||||||
return _values(style.$, 'Style: {name},{font_name},{font_size}, ' +
|
|
||||||
'{primary_colour},{secondary_colour},{outline_colour}, ' +
|
|
||||||
'{back_colour},{bold},{italic},{underline},{strikeout},{scale_x}, ' +
|
|
||||||
'{scale_y},{spacing},{angle},{border_style},{outline},{shadow},' +
|
|
||||||
'{alignment},{margin_l},{margin_r},{margin_v},{encoding}');
|
|
||||||
});
|
|
||||||
return '[V4+ Styles]\n' +
|
|
||||||
'Format: ' + format + '\n' +
|
|
||||||
items.join('\n') + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills a predetermined format with the values from the attributes.
|
|
||||||
* @param {Object.<string, *>} attributes
|
|
||||||
* @param {string} value
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _values(attributes, format) {
|
|
||||||
return format.replace(/{([^}]+)}/g, function(match, key) {
|
|
||||||
return attributes[key] || '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
93
src/subtitle/formats/ass.ts
Normal file
93
src/subtitle/formats/ass.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
'use strict';
|
||||||
|
export = main;
|
||||||
|
import xml2js = require('xml2js');
|
||||||
|
import typings = require('../../typings');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an input buffer to a SubStation Alpha subtitle.
|
||||||
|
*/
|
||||||
|
function main(input: string|Buffer, done: (err: Error, subtitle?: string) => void) {
|
||||||
|
xml2js.parseString(input.toString(), {
|
||||||
|
explicitArray: false,
|
||||||
|
explicitRoot: false
|
||||||
|
}, (err: Error, xml: typings.ISubtitle) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
try {
|
||||||
|
done(null, script(xml) + '\n' +
|
||||||
|
style(xml.styles) + '\n' +
|
||||||
|
event(xml.events));
|
||||||
|
} catch (err) {
|
||||||
|
done(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the event block.
|
||||||
|
*/
|
||||||
|
function event(block: typings.ISubtitleEvent): string {
|
||||||
|
var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
||||||
|
return '[Events]\n' +
|
||||||
|
'Format: ' + format + '\n' +
|
||||||
|
block.event.map(style => ('Dialogue: 0,' +
|
||||||
|
style.$.start + ',' +
|
||||||
|
style.$.end + ',' +
|
||||||
|
style.$.style + ',' +
|
||||||
|
style.$.name + ',' +
|
||||||
|
style.$.margin_l + ',' +
|
||||||
|
style.$.margin_r + ',' +
|
||||||
|
style.$.margin_v + ',' +
|
||||||
|
style.$.effect + ',' +
|
||||||
|
style.$.text)).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the script block.
|
||||||
|
*/
|
||||||
|
function script(block: typings.ISubtitle): string {
|
||||||
|
return '[Script Info]\n' +
|
||||||
|
'Title: ' + block.$.title + '\n' +
|
||||||
|
'ScriptType: v4.00+\n' +
|
||||||
|
'WrapStyle: ' + block.$.wrap_style + '\n' +
|
||||||
|
'PlayResX: ' + block.$.play_res_x + '\n' +
|
||||||
|
'PlayResY: ' + block.$.play_res_y + '\n' +
|
||||||
|
'Subtitle ID: ' + block.$.id + '\n' +
|
||||||
|
'Language: ' + block.$.lang_string + '\n' +
|
||||||
|
'Created: ' + block.$.created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the style block.
|
||||||
|
*/
|
||||||
|
function style(block: typings.ISubtitleStyle): string {
|
||||||
|
var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
|
||||||
|
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
|
||||||
|
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
|
||||||
|
'MarginL,MarginR,MarginV,Encoding';
|
||||||
|
return '[V4+ Styles]\n' +
|
||||||
|
'Format: ' + format + '\n' +
|
||||||
|
block.style.map(style => 'Style: ' +
|
||||||
|
style.$.name + ',' +
|
||||||
|
style.$.font_name + ',' +
|
||||||
|
style.$.font_size + ',' +
|
||||||
|
style.$.primary_colour + ',' +
|
||||||
|
style.$.secondary_colour + ',' +
|
||||||
|
style.$.outline_colour + ',' +
|
||||||
|
style.$.back_colour + ',' +
|
||||||
|
style.$.bold + ',' +
|
||||||
|
style.$.italic + ',' +
|
||||||
|
style.$.underline + ',' +
|
||||||
|
style.$.strikeout + ',' +
|
||||||
|
style.$.scale_x + ',' +
|
||||||
|
style.$.scale_y + ',' +
|
||||||
|
style.$.spacing + ',' +
|
||||||
|
style.$.angle + ',' +
|
||||||
|
style.$.border_style + ',' +
|
||||||
|
style.$.outline + ',' +
|
||||||
|
style.$.shadow + ',' +
|
||||||
|
style.$.alignment + ',' +
|
||||||
|
style.$.margin_l + ',' +
|
||||||
|
style.$.margin_r + ',' +
|
||||||
|
style.$.margin_v + ',' +
|
||||||
|
style.$.encoding).join('\n');
|
||||||
|
}
|
||||||
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
ass: require('./ass'),
|
|
||||||
srt: require('./srt')
|
|
||||||
};
|
|
||||||
10
src/subtitle/formats/index.ts
Normal file
10
src/subtitle/formats/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
export = main;
|
||||||
|
import ass = require('./ass');
|
||||||
|
import srt = require('./srt');
|
||||||
|
import typings = require('../../typings');
|
||||||
|
|
||||||
|
var main: typings.IFormatterTable = {
|
||||||
|
ass: ass,
|
||||||
|
srt: srt
|
||||||
|
};
|
||||||
@ -1,92 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var xml2js = require('xml2js');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an input buffer to a SubRip subtitle.
|
|
||||||
* @param {Buffer|string} input
|
|
||||||
* @param {function(Error, string=)} done
|
|
||||||
*/
|
|
||||||
module.exports = function(input, done) {
|
|
||||||
if (typeof buffer !== 'string') input = input.toString();
|
|
||||||
xml2js.parseString(input, {
|
|
||||||
explicitArray: false,
|
|
||||||
explicitRoot: false
|
|
||||||
}, function(err, xml) {
|
|
||||||
try {
|
|
||||||
if (err) return done(err);
|
|
||||||
done(undefined, xml.events.event.map(_event).join('\n'));
|
|
||||||
} catch(err) {
|
|
||||||
done(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an event.
|
|
||||||
* @private
|
|
||||||
* @param {Object} event
|
|
||||||
* @param {number} index
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _event(event, index) {
|
|
||||||
var attributes = event.$;
|
|
||||||
return (index + 1) + '\n' +
|
|
||||||
_time(attributes.start) + ' --> ' + _time(attributes.end) + '\n' +
|
|
||||||
_text(attributes.text) + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefixes a value.
|
|
||||||
* @private
|
|
||||||
* @param {string} value
|
|
||||||
* @param {number} length
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _prefix(value, length) {
|
|
||||||
while (value.length < length) value = '0' + value;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suffixes a value.
|
|
||||||
* @private
|
|
||||||
* @param {string} value
|
|
||||||
* @param {number} length
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _suffix(value, length) {
|
|
||||||
while (value.length < length) value = value + '0';
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a text value.
|
|
||||||
* @private
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _text(text) {
|
|
||||||
return text
|
|
||||||
.replace(/{\\i1}/g, '<i>').replace(/{\\i0}/g, '</i>')
|
|
||||||
.replace(/{\\b1}/g, '<b>').replace(/{\\b0}/g, '</b>')
|
|
||||||
.replace(/{\\u1}/g, '<u>').replace(/{\\u0}/g, '</u>')
|
|
||||||
.replace(/{[^}]+}/g, '')
|
|
||||||
.replace(/(\s+)?\\n(\s+)?/ig, '\n')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a time stamp.
|
|
||||||
* @private
|
|
||||||
* @param {string} time
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _time(time) {
|
|
||||||
var all = time.match(/^([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)$/);
|
|
||||||
if (!all) throw new Error('Invalid time.');
|
|
||||||
var hours = _prefix(all[1], 2);
|
|
||||||
var minutes = _prefix(all[2], 2);
|
|
||||||
var seconds = _prefix(all[3], 2);
|
|
||||||
var milliseconds = _suffix(all[4], 3);
|
|
||||||
return hours + ':' + minutes + ':' + seconds + ',' + milliseconds;
|
|
||||||
}
|
|
||||||
66
src/subtitle/formats/srt.ts
Normal file
66
src/subtitle/formats/srt.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
'use strict';
|
||||||
|
export = srt;
|
||||||
|
import xml2js = require('xml2js');
|
||||||
|
import typings = require('../../typings');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an input buffer to a SubRip subtitle.
|
||||||
|
*/
|
||||||
|
function srt(input: Buffer|string, done: (err: Error, subtitle?: string) => void) {
|
||||||
|
var options = {explicitArray: false, explicitRoot: false};
|
||||||
|
xml2js.parseString(input.toString(), options, (err: Error, xml: typings.ISubtitle) => {
|
||||||
|
try {
|
||||||
|
if (err) return done(err);
|
||||||
|
done(null, xml.events.event.map((event, index) => {
|
||||||
|
var attributes = event.$;
|
||||||
|
return (index + 1) + '\n' +
|
||||||
|
time(attributes.start) + ' --> ' + time(attributes.end) + '\n' +
|
||||||
|
text(attributes.text) + '\n';
|
||||||
|
}).join('\n'));
|
||||||
|
} catch (err) {
|
||||||
|
done(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefixes a value.
|
||||||
|
*/
|
||||||
|
function prefix(value: string, length: number): string {
|
||||||
|
while (value.length < length) value = '0' + value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suffixes a value.
|
||||||
|
*/
|
||||||
|
function suffix(value: string, length: number): string {
|
||||||
|
while (value.length < length) value = value + '0';
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a text value.
|
||||||
|
*/
|
||||||
|
function text(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/{\\i1}/g, '<i>').replace(/{\\i0}/g, '</i>')
|
||||||
|
.replace(/{\\b1}/g, '<b>').replace(/{\\b0}/g, '</b>')
|
||||||
|
.replace(/{\\u1}/g, '<u>').replace(/{\\u0}/g, '</u>')
|
||||||
|
.replace(/{[^}]+}/g, '')
|
||||||
|
.replace(/(\s+)?\\n(\s+)?/ig, '\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a time stamp.
|
||||||
|
*/
|
||||||
|
function time(value: string): string {
|
||||||
|
var all = value.match(/^([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)$/);
|
||||||
|
if (!all) throw new Error('Invalid time.');
|
||||||
|
var hours = prefix(all[1], 2);
|
||||||
|
var minutes = prefix(all[2], 2);
|
||||||
|
var seconds = prefix(all[3], 2);
|
||||||
|
var milliseconds = suffix(all[4], 3);
|
||||||
|
return hours + ':' + minutes + ':' + seconds + ',' + milliseconds;
|
||||||
|
}
|
||||||
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
decode: require('./decode'),
|
|
||||||
formats: require('./formats')
|
|
||||||
};
|
|
||||||
3
src/subtitle/index.ts
Normal file
3
src/subtitle/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
'use strict';
|
||||||
|
export import decode = require('./decode');
|
||||||
|
export import formats = require('./formats/index');
|
||||||
136
src/typings.ts
Normal file
136
src/typings.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
export interface IConfig {
|
||||||
|
// Authentication
|
||||||
|
pass?: string;
|
||||||
|
user?: string;
|
||||||
|
// Disables
|
||||||
|
cache?: boolean;
|
||||||
|
merge?: boolean;
|
||||||
|
// Filters
|
||||||
|
episode?: number;
|
||||||
|
volume?: number;
|
||||||
|
// Settings
|
||||||
|
format?: string;
|
||||||
|
output?: string;
|
||||||
|
series?: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConfigLine extends IConfig {
|
||||||
|
args: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConfigTask {
|
||||||
|
address: string;
|
||||||
|
config: IConfigLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEpisodePage {
|
||||||
|
id: number;
|
||||||
|
episode: number;
|
||||||
|
series: string;
|
||||||
|
volume: number;
|
||||||
|
swf: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEpisodePlayer {
|
||||||
|
subtitle: {
|
||||||
|
id: number;
|
||||||
|
iv: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
video: {
|
||||||
|
file: string;
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEpisodePlayerConfig {
|
||||||
|
'default:preload': {
|
||||||
|
subtitle: {
|
||||||
|
$: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
iv: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
stream_info: {
|
||||||
|
file: string;
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFormatterTable {
|
||||||
|
[key: string]: (input: string|Buffer, done: (err: Error, subtitle?: string) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISeries {
|
||||||
|
episodes: ISeriesEpisode[];
|
||||||
|
series: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISeriesEpisode {
|
||||||
|
address: string;
|
||||||
|
episode: number;
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISubtitle {
|
||||||
|
$: {
|
||||||
|
title: string;
|
||||||
|
wrap_style: string;
|
||||||
|
play_res_x: string;
|
||||||
|
play_res_y: string;
|
||||||
|
id: string;
|
||||||
|
lang_string: string;
|
||||||
|
created: string;
|
||||||
|
};
|
||||||
|
events: ISubtitleEvent;
|
||||||
|
styles: ISubtitleStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISubtitleEvent {
|
||||||
|
event: {
|
||||||
|
$: {
|
||||||
|
end: string;
|
||||||
|
start: string;
|
||||||
|
style: string;
|
||||||
|
name: string;
|
||||||
|
margin_l: string;
|
||||||
|
margin_r: string;
|
||||||
|
margin_v: string;
|
||||||
|
effect: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISubtitleStyle {
|
||||||
|
style: {
|
||||||
|
$: {
|
||||||
|
name: string;
|
||||||
|
font_name: string;
|
||||||
|
font_size: string;
|
||||||
|
primary_colour: string;
|
||||||
|
secondary_colour: string;
|
||||||
|
outline_colour: string;
|
||||||
|
back_colour: string;
|
||||||
|
bold: string;
|
||||||
|
italic: string;
|
||||||
|
underline: string;
|
||||||
|
strikeout: string;
|
||||||
|
scale_x: string;
|
||||||
|
scale_y: string;
|
||||||
|
spacing: string;
|
||||||
|
angle: string;
|
||||||
|
border_style: string;
|
||||||
|
outline: string;
|
||||||
|
shadow: string;
|
||||||
|
alignment: string;
|
||||||
|
margin_l: string;
|
||||||
|
margin_r: string;
|
||||||
|
margin_v: string;
|
||||||
|
encoding: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
merge: require('./merge'),
|
|
||||||
stream: require('./stream')
|
|
||||||
};
|
|
||||||
3
src/video/index.ts
Normal file
3
src/video/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
'use strict';
|
||||||
|
export import merge = require('./merge');
|
||||||
|
export import stream = require('./stream');
|
||||||
@ -1,71 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var childProcess = require('child_process');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
var os = require('os');
|
|
||||||
var subtitle = require('../subtitle');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges the subtitle and video files into a Matroska Multimedia Container.
|
|
||||||
* @param {Object} config
|
|
||||||
* @param {string} rtmpInputPath
|
|
||||||
* @param {string} filePath
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
module.exports = function(config, rtmpInputPath, filePath, done) {
|
|
||||||
var format = subtitle.formats[config.format] ? config.format : 'ass';
|
|
||||||
var subtitlePath = filePath + '.' + format;
|
|
||||||
var videoPath = filePath + path.extname(rtmpInputPath);
|
|
||||||
childProcess.exec(_command() + ' ' +
|
|
||||||
'-o "' + filePath + '.mkv" ' +
|
|
||||||
'"' + videoPath + '" ' +
|
|
||||||
'"' + subtitlePath + '"', {
|
|
||||||
maxBuffer: Infinity
|
|
||||||
}, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
_unlink(videoPath, subtitlePath, function(err) {
|
|
||||||
if (err) _unlinkTimeout(videoPath, subtitlePath, 5000);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the command for the operating system.
|
|
||||||
* @private
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _command() {
|
|
||||||
if (os.platform() !== 'win32') return 'mkvmerge';
|
|
||||||
return path.join(__dirname, '../../bin/mkvmerge.exe');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlinks the video and subtitle.
|
|
||||||
* @private
|
|
||||||
* @param {string} videoPath
|
|
||||||
* @param {string} subtitlePath
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function _unlink(videoPath, subtitlePath, done) {
|
|
||||||
fs.unlink(videoPath, function(err) {
|
|
||||||
if (err) return done(err);
|
|
||||||
fs.unlink(subtitlePath, done);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to unlink the video and subtitle with a timeout between each try.
|
|
||||||
* @private
|
|
||||||
* @param {string} videoPath
|
|
||||||
* @param {string} subtitlePath
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
function _unlinkTimeout(videoPath, subtitlePath, timeout) {
|
|
||||||
console.log('Trying to unlink...' + Date.now());
|
|
||||||
setTimeout(function() {
|
|
||||||
_unlink(videoPath, subtitlePath, function(err) {
|
|
||||||
if (err) _unlinkTimeout(videoPath, subtitlePath, timeout);
|
|
||||||
});
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
58
src/video/merge.ts
Normal file
58
src/video/merge.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
'use strict';
|
||||||
|
export = main;
|
||||||
|
import childProcess = require('child_process');
|
||||||
|
import fs = require('fs');
|
||||||
|
import path = require('path');
|
||||||
|
import os = require('os');
|
||||||
|
import subtitle = require('../subtitle/index');
|
||||||
|
import typings = require('../typings');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the subtitle and video files into a Matroska Multimedia Container.
|
||||||
|
*/
|
||||||
|
function main(config: typings.IConfig, rtmpInputPath: string, filePath: string, done: (err: Error) => void) {
|
||||||
|
var subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
|
||||||
|
var videoPath = filePath + path.extname(rtmpInputPath);
|
||||||
|
childProcess.exec(command() + ' ' +
|
||||||
|
'-o "' + filePath + '.mkv" ' +
|
||||||
|
'"' + videoPath + '" ' +
|
||||||
|
'"' + subtitlePath + '"', {
|
||||||
|
maxBuffer: Infinity
|
||||||
|
}, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
unlink(videoPath, subtitlePath, err => {
|
||||||
|
if (err) unlinkTimeout(videoPath, subtitlePath, 5000);
|
||||||
|
done(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the command for the operating system.
|
||||||
|
*/
|
||||||
|
function command(): string {
|
||||||
|
if (os.platform() !== 'win32') return 'mkvmerge';
|
||||||
|
return path.join(__dirname, '../../bin/mkvmerge.exe');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlinks the video and subtitle.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function unlink(videoPath: string, subtitlePath: string, done: (err: Error) => void) {
|
||||||
|
fs.unlink(videoPath, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
fs.unlink(subtitlePath, done);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to unlink the video and subtitle with a timeout between each try.
|
||||||
|
*/
|
||||||
|
function unlinkTimeout(videoPath: string, subtitlePath: string, timeout: number) {
|
||||||
|
setTimeout(() => {
|
||||||
|
unlink(videoPath, subtitlePath, err => {
|
||||||
|
if (err) unlinkTimeout(videoPath, subtitlePath, timeout);
|
||||||
|
});
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
@ -1,32 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
var childProcess = require('child_process');
|
|
||||||
var path = require('path');
|
|
||||||
var os = require('os');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Streams the video to disk.
|
|
||||||
* @param {string} rtmpUrl
|
|
||||||
* @param {string} rtmpInputPath
|
|
||||||
* @param {string} swfUrl
|
|
||||||
* @param {string} filePath
|
|
||||||
* @param {function(Error)} done
|
|
||||||
*/
|
|
||||||
module.exports = function(rtmpUrl, rtmpInputPath, swfUrl, filePath, done) {
|
|
||||||
childProcess.exec(_command() + ' ' +
|
|
||||||
'-r "' + rtmpUrl + '" ' +
|
|
||||||
'-y "' + rtmpInputPath + '" ' +
|
|
||||||
'-W "' + swfUrl + '" ' +
|
|
||||||
'-o "' + filePath + '"', {
|
|
||||||
maxBuffer: Infinity
|
|
||||||
}, done);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the command for the operating system.
|
|
||||||
* @private
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function _command() {
|
|
||||||
if (os.platform() !== 'win32') return 'rtmpdump';
|
|
||||||
return path.join(__dirname, '../../bin/rtmpdump.exe');
|
|
||||||
}
|
|
||||||
26
src/video/stream.ts
Normal file
26
src/video/stream.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
export = main;
|
||||||
|
import childProcess = require('child_process');
|
||||||
|
import path = require('path');
|
||||||
|
import os = require('os');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the video to disk.
|
||||||
|
*/
|
||||||
|
function main(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string, done: (err: Error) => void) {
|
||||||
|
childProcess.exec(command() + ' ' +
|
||||||
|
'-r "' + rtmpUrl + '" ' +
|
||||||
|
'-y "' + rtmpInputPath + '" ' +
|
||||||
|
'-W "' + swfUrl + '" ' +
|
||||||
|
'-o "' + filePath + '"', {
|
||||||
|
maxBuffer: Infinity
|
||||||
|
}, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the command for the operating system.
|
||||||
|
*/
|
||||||
|
function command(): string {
|
||||||
|
if (os.platform() !== 'win32') return 'rtmpdump';
|
||||||
|
return path.join(__dirname, '../../bin/rtmpdump.exe');
|
||||||
|
}
|
||||||
33
tsd.json
Normal file
33
tsd.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"version": "v4",
|
||||||
|
"repo": "borisyankov/DefinitelyTyped",
|
||||||
|
"ref": "master",
|
||||||
|
"path": "typings",
|
||||||
|
"bundle": "typings/tsd.d.ts",
|
||||||
|
"installed": {
|
||||||
|
"node/node.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
},
|
||||||
|
"commander/commander.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
},
|
||||||
|
"xml2js/xml2js.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
},
|
||||||
|
"cheerio/cheerio.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
},
|
||||||
|
"mkdirp/mkdirp.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
},
|
||||||
|
"request/request.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
},
|
||||||
|
"big-integer/big-integer.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
},
|
||||||
|
"form-data/form-data.d.ts": {
|
||||||
|
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user