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/
|
||||
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`
|
||||
* Windows examples with a .bat for ease of use.
|
||||
* Publish to `npm` with a fixed package.json.
|
||||
* Conversion to beautiful TypeScript 1.4 code.
|
||||
|
||||
### Pending Implementation
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
var path = require('path');
|
||||
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",
|
||||
"repository": "git://github.com/Deathspike/crunchyroll.js.git",
|
||||
"version": "1.0.6",
|
||||
"version": "1.1.0",
|
||||
"bin": {
|
||||
"crunchyroll": "./bin/crunchyroll"
|
||||
},
|
||||
"dependencies": {
|
||||
"big-integer": "^1.4.1",
|
||||
"big-integer": "^1.4.3",
|
||||
"cheerio": "^0.18.0",
|
||||
"commander": "^2.6.0",
|
||||
"mkdirp": "^0.5.0",
|
||||
"request": "^2.51.0",
|
||||
"request": "^2.53.0",
|
||||
"xml2js": "^0.4.4"
|
||||
},
|
||||
"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';
|
||||
var Command = require('commander').Command;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var series = require('./series');
|
||||
export = main;
|
||||
import commander = require('commander');
|
||||
import fs = require('fs');
|
||||
import path = require('path');
|
||||
import series = require('./series');
|
||||
import typings = require('./typings');
|
||||
|
||||
/**
|
||||
* Streams the batch of series to disk.
|
||||
* @param {Array.<string>} args
|
||||
* @param {function(Error)} done
|
||||
*/
|
||||
module.exports = function(args, done) {
|
||||
var config = _parse(args);
|
||||
function main(args: string[], done: (err?: Error) => void) {
|
||||
var config = parse(args);
|
||||
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);
|
||||
var i = 0;
|
||||
(function next() {
|
||||
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);
|
||||
i += 1;
|
||||
next();
|
||||
});
|
||||
})();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the value into arguments.
|
||||
* @private
|
||||
* @param {string} value
|
||||
* @returns {Array.<string>}
|
||||
*/
|
||||
function _split(value) {
|
||||
function split(value: string): string[] {
|
||||
var inQuote = false;
|
||||
var pieces = [];
|
||||
var i: number;
|
||||
var pieces: string[] = [];
|
||||
var previous = 0;
|
||||
for (var i = 0; i < value.length; i += 1) {
|
||||
if (value.charAt(i) === '"') {
|
||||
inQuote = !inQuote;
|
||||
}
|
||||
for (i = 0; i < value.length; i += 1) {
|
||||
if (value.charAt(i) === '"') inQuote = !inQuote;
|
||||
if (!inQuote && value.charAt(i) === ' ') {
|
||||
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]);
|
||||
previous = i + 1;
|
||||
@ -51,43 +47,36 @@ function _split(value) {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return done(undefined, config.args.map(function(address) {
|
||||
return done(null, config.args.map(address => {
|
||||
return {address: address, config: config};
|
||||
}));
|
||||
}
|
||||
fs.exists(batchPath, function(exists) {
|
||||
if (!exists) return done(undefined, []);
|
||||
fs.readFile(batchPath, 'utf8', function(err, data) {
|
||||
fs.exists(batchPath, exists => {
|
||||
if (!exists) return done(null, []);
|
||||
fs.readFile(batchPath, 'utf8', (err, data) => {
|
||||
if (err) return done(err);
|
||||
var map = [];
|
||||
data.split(/\r?\n/).forEach(function(line) {
|
||||
var map: typings.IConfigTask[] = [];
|
||||
data.split(/\r?\n/).forEach(line => {
|
||||
if (/^(\/\/|#)/.test(line)) return;
|
||||
var lineConfig = _parse(process.argv.concat(_split(line)));
|
||||
lineConfig.args.forEach(function(address) {
|
||||
var lineConfig = parse(process.argv.concat(split(line)));
|
||||
lineConfig.args.forEach(address => {
|
||||
if (!address) return;
|
||||
map.push({address: address, config: lineConfig});
|
||||
});
|
||||
});
|
||||
done(undefined, map);
|
||||
done(null, map);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the arguments and returns a configuration.
|
||||
* @private
|
||||
* @param {Array.<string>} args
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _parse(args) {
|
||||
return new Command().version(require('../package').version)
|
||||
function parse(args: string[]): typings.IConfigLine {
|
||||
return new commander.Command().version(require('../package').version)
|
||||
// Authentication
|
||||
.option('-p, --pass <s>', 'The password.')
|
||||
.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';
|
||||
var cheerio = require('cheerio');
|
||||
var episode = require('./episode');
|
||||
export = main;
|
||||
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 fs = require('fs');
|
||||
var request = require('./request');
|
||||
var path = require('path');
|
||||
var url = require('url');
|
||||
|
||||
/**
|
||||
* 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);
|
||||
fs.readFile(persistentPath, 'utf8', function(err, contents) {
|
||||
fs.readFile(persistentPath, 'utf8', (err, contents) => {
|
||||
var cache = config.cache ? {} : JSON.parse(contents || '{}');
|
||||
_page(config, address, function(err, page) {
|
||||
page(config, address, (err, page) => {
|
||||
if (err) return done(err);
|
||||
var i = 0;
|
||||
(function next() {
|
||||
if (i >= page.episodes.length) return done();
|
||||
_download(cache, config, address, page.episodes[i], function(err) {
|
||||
if (i >= page.episodes.length) return done(null);
|
||||
download(cache, config, address, page.episodes[i], err => {
|
||||
if (err) return done(err);
|
||||
var newCache = JSON.stringify(cache, null, ' ');
|
||||
fs.writeFile(persistentPath, newCache, function(err) {
|
||||
fs.writeFile(persistentPath, newCache, err => {
|
||||
if (err) return done(err);
|
||||
i += 1;
|
||||
next();
|
||||
@ -34,42 +33,33 @@ module.exports = function(config, address, done) {
|
||||
})();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!_filter(config, item)) return 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(null);
|
||||
var address = url.resolve(baseAddress, item.address);
|
||||
if (cache[address]) return done();
|
||||
episode(config, address, function(err) {
|
||||
if (cache[address]) return done(null);
|
||||
episode(config, address, err => {
|
||||
if (err) return done(err);
|
||||
cache[address] = Date.now();
|
||||
done();
|
||||
done(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
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;
|
||||
|
||||
// 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;
|
||||
return true;
|
||||
@ -77,19 +67,15 @@ function _filter(config, item) {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
request.get(config, address, function(err, res, body) {
|
||||
function page(config: typings.IConfig, address: string, done: (err: Error, result?: typings.ISeries) => void) {
|
||||
request.get(config, address, (err, result) => {
|
||||
if (err) return done(err);
|
||||
var $ = cheerio.load(body);
|
||||
var $ = cheerio.load(result);
|
||||
var title = $('span[itemprop=name]').text();
|
||||
if (!title) return done(new Error('Invalid page.'));
|
||||
var episodes = [];
|
||||
$('.episode').each(function(i, el) {
|
||||
var episodes: typings.ISeriesEpisode[] = [];
|
||||
$('.episode').each((i, el) => {
|
||||
if ($(el).children('img[src*=coming_soon]').length) return;
|
||||
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());
|
||||
var regexp = /Episode\s+([0-9]+)\s*$/i;
|
||||
@ -102,6 +88,6 @@ function _page(config, address, done) {
|
||||
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