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:
Roel van Uden 2015-02-06 22:31:02 +01:00
parent d01b204cce
commit 9ff6398aee
38 changed files with 1014 additions and 909 deletions

7
.gitignore vendored
View File

@ -1 +1,8 @@
dist/
node_modules/
obj/
typings/
*.dat
*.dll
*.suo
*.tmp

60
.jscsrc
View File

@ -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"
}

View File

@ -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" : []
}

View File

@ -1 +1,11 @@
extras/
node_modules/
obj/
src/
typings/
*.dat
*.dll
*.map
*.njsproj
*.sln
*.suo
*.tmp

View File

@ -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

View File

@ -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
View 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
View 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

View 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">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="TypeScript"&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;JsReformatCode&gt;True&lt;/JsReformatCode&gt;&lt;JsFormatDocComments&gt;True&lt;/JsFormatDocComments&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;RemoveRedundantQualifiersTs&gt;True&lt;/RemoveRedundantQualifiersTs&gt;&lt;OptimizeImportsTs&gt;True&lt;/OptimizeImportsTs&gt;&lt;OptimizeReferenceCommentsTs&gt;True&lt;/OptimizeReferenceCommentsTs&gt;&lt;PublicModifierStyleTs&gt;True&lt;/PublicModifierStyleTs&gt;&lt;RelativePathStyleTs&gt;True&lt;/RelativePathStyleTs&gt;&lt;TypeAnnotationStyleTs&gt;True&lt;/TypeAnnotationStyleTs&gt;&lt;/Profile&gt;</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">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FCONSTRUCTOR/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FFUNCTION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FGLOBAL_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLABEL/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLOCAL_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FOBJECT_005FPROPERTY_005FOF_005FFUNCTION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FPARAMETER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FCLASS/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM_005FMEMBER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FINTERFACE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FINTERFACE_005FFOR_005FJS_005FGLOBAL_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FEXPORTED/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FLOCAL/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FMEMBER_005FACCESSOR/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FMETHOD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FMEMBER_005FACCESSOR/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FMETHOD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FMEMBER_005FACCESSOR/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FMETHOD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FTYPE_005FPARAMETER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /&gt;</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>

View File

@ -1,6 +0,0 @@
'use strict';
var src = require('./src');
src.batch(process.argv, function(err) {
if (err) return console.error(err.stack || err);
});

View File

@ -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"
}
}

View File

@ -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
View File

@ -0,0 +1,6 @@
'use strict';
import batch = require('./batch');
batch(process.argv, err => {
if (err) console.error(err);
});

View File

@ -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
View 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);
}
});
});
}

View File

@ -1,5 +0,0 @@
module.exports = {
batch: require('./batch'),
episode: require('./episode'),
series: require('./series')
};

4
src/index.ts Normal file
View File

@ -0,0 +1,4 @@
'use strict';
export import batch = require('./batch');
export import episode = require('./episode');
export import series = require('./series');

View File

@ -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
View 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()};
}

View File

@ -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) {
* Filters the item based on the configuration.
*/
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});
});
}

View File

@ -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
View 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;
}

View File

@ -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] || '';
});
}

View 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');
}

View File

@ -1,4 +0,0 @@
module.exports = {
ass: require('./ass'),
srt: require('./srt')
};

View 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
};

View File

@ -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;
}

View 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;
}

View File

@ -1,4 +0,0 @@
module.exports = {
decode: require('./decode'),
formats: require('./formats')
};

3
src/subtitle/index.ts Normal file
View File

@ -0,0 +1,3 @@
'use strict';
export import decode = require('./decode');
export import formats = require('./formats/index');

136
src/typings.ts Normal file
View 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;
};
}[];
}

View File

@ -1,4 +0,0 @@
module.exports = {
merge: require('./merge'),
stream: require('./stream')
};

3
src/video/index.ts Normal file
View File

@ -0,0 +1,3 @@
'use strict';
export import merge = require('./merge');
export import stream = require('./stream');

View File

@ -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
View 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);
}

View File

@ -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
View 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
View 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"
}
}
}