21 Commits

Author SHA1 Message Date
Godzil
62a08e14bb Add missing mode parameter to the stream object. (and correct call to it) 2016-08-13 21:24:57 +01:00
Godzil
422d0827f9 Correct file source extension for the merge pass. 2016-08-13 16:32:56 +01:00
Godzil
546a849aa5 Add ffmpeg when using HLS instead of RTMP. 2016-08-13 16:20:33 +01:00
Roel van Uden
e06ff53210 Lock down dependencies, bump version (#9) 2015-06-25 19:42:10 +02:00
Roel van Uden
18375d3d22 Update to TS1.5 2015-05-23 19:07:14 +02:00
Roel van Uden
5fdee94b38 References for future self. 2015-03-07 18:46:42 +01:00
Roel van Uden
7145f72635 Bump the version 2015-03-07 13:31:30 +01:00
Roel van Uden
4f613ad45c Quick developer updates to develop with atom 2015-03-07 13:31:15 +01:00
Roel van Uden
1afcef88a0 #6: Support subtitles without multiple styles. 2015-03-07 13:30:02 +01:00
Roel van Uden
1288d0b3f8 Notes for future me 2015-03-07 13:29:36 +01:00
Roel van Uden
2bb5feb647 Generate source maps with tsconfig 2015-03-07 13:29:29 +01:00
Roel van Uden
602f772fcf Get rid of Visual Studio project files for tsconfig 2015-03-07 11:52:10 +01:00
Roel van Uden
887b3ed094 Bump the version 2015-03-06 22:22:53 +01:00
Roel van Uden
49e3290f28 Fixing the README while I'm at it 2015-03-06 22:17:45 +01:00
Roel van Uden
5d32d91d7d #5: Enable merging subtitle-less. 2015-03-06 22:12:03 +01:00
Roel van Uden
2f1858cde7 #5: Support subtitle-less videos 2015-03-06 21:58:31 +01:00
Roel van Uden
a98ed223c6 #5: Support drama videos in regex 2015-03-06 21:50:31 +01:00
Roel van Uden
575569bd91 #4: Support spaces in Windows path 2015-03-06 21:41:02 +01:00
Roel van Uden
44a66286cb Major update of README.md 2015-02-28 13:48:02 +01:00
Roel van Uden
eb7de600c1 Update typing dependencies. 2015-02-28 13:28:59 +01:00
Roel van Uden
d2e8a4c02e Update ts.js 2015-02-07 13:29:10 +01:00
38 changed files with 438 additions and 524 deletions

5
.gitignore vendored
View File

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

View File

@@ -1,16 +1,8 @@
extras/ extras/
node_modules/ node_modules/
obj/
src/ src/
typings/ typings/
*.dat
*.DotSettings
*.dll
*.map
*.njsproj
*.sln
*.suo
*.tmp
ts.js ts.js
tsconfig.json
tsd.json tsd.json
tslint.json tslint.json

154
README.md
View File

@@ -1,69 +1,117 @@
# CrunchyRoll.js # CrunchyRoll.js
*CrunchyRoll.js* is capable of downloading *anime* episodes from the popular *CrunchyRoll.js* is capable of downloading *anime* episodes from the popular *CrunchyRoll* streaming service. An episode is stored in the original video format (often H.264 in a MP4 container) and the configured subtitle format (ASS or SRT).The two output files are then merged into a single MKV file.
*CrunchyRoll* streaming service. An episode is stored in the original video format
(often H.264 in a MP4 container) and the configured subtitle format (ASS or
SRT).The two output files are then merged into a single MKV file.
## Motivation ## Motivation
*CrunchyRoll* has been providing an amazing streaming service and offers the *CrunchyRoll* has been providing an amazing streaming service and offers the best way to enjoy *anime* in a *convenient* and *legal* way. As a streaming service, video files cannot be downloaded and watched offline. Understandable from a business perspective and considering possible contract implications, but annoying for users. This application enables episodes to be downloaded for offline convenience. Please do not abuse this application; download episodes for **personal use** and **delete them** if you do not have an active premium account. Continue to support *CrunchyRoll*; without our financial backing their service cannot exist!
best way to enjoy *anime* in a *convenient* and *legal* way. As a streaming
service, video files cannot be downloaded and watched offline. Understandable
from a business perspective and considering possible contract implications, but
annoying for users. This application enables episodes to be downloaded for
offline convenience. Please do not abuse this application; download episodes for
**personal use** and **delete them** if you do not have an active premium
account. Continue to support *CrunchyRoll*; without our financial backing their
service cannot exist!
## Legal Warning ## Legal Warning
This application is not endorsed or affliated with *CrunchyRoll*. The usage of This application is not endorsed or affliated with *CrunchyRoll*. The usage of this application enables episodes to be downloaded for offline convenience which may be forbidden by law in your country. Usage of this application may also cause a violation of the agreed *Terms of Service* between you and the stream provider. A tool is not responsible for your actions; please make an informed decision prior to using this application.
this application enables episodes to be downloaded for offline convenience which
may be forbidden by law in your country. Usage of this application may also
cause a violation of the agreed *Terms of Service* between you and the stream
provider. A tool is not responsible for your actions; please make an informed
decision prior to using this application.
## Status
### Implemented
* Subtitle decoding.
* Subtitle converter for SRT subtitle output.
* Video streaming.
* Episode page scraping with subtitle saving and video streaming.
* Add ASS support.
* Add muxing (MP4+ASS=MKV).
* Add series API to save an entire series rather than per-episode.
* Add support for incremental saves.
* Add batch-mode to queue a bunch of series.
* Add CLI interface with all the options.
* Support scheduled merging; if it fails now, the video is probably being watched.
* Add authentication to the entire stack to support premium content.
* 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
* Documentation.
* Enjoy beautiful anime series from disk when internet is down.
## Configuration ## Configuration
Set defaults in https://www.crunchyroll.com/acct/?action=video. We'll use that. It is recommended to enable authentication (`-p` and `-u`) so your account permissions and settings are available for use. It is not possible to download non-free material without an account and premium subscription. Furthermore, the default account settings are used when downloading. If you want the highest quality videos, configure these preferences at https://www.crunchyroll.com/acct/?action=video.
## Prerequisites
* NodeJS >= 0.12.x (http://nodejs.org/)
* NPM >= 2.5.x (https://www.npmjs.org/)
## Installation
Use the applicable instructions to install. Is your operating system not listed? Please ask or contribute!
### Debian (Mint, Ubuntu, etc)
1. Run in *Terminal*: `sudo apt-get install nodejs npm mkvtoolnix rtmpdump ffmpeg`
2. Run in *Terminal*: `sudo ln -s /usr/bin/nodejs /usr/bin/node`
3. Run in *Terminal*: `sudo npm install -g crunchyroll`
### Mac OS X
1. Install *Homebrew* following the instructions at http://brew.sh/
2. Run in *Terminal*: `brew install node mkvtoolnix rtmpdump ffmpeg`
3. Run in *Terminal*: `npm install -g crunchyroll`
### Windows
1. Install *NodeJS* following the instructions at http://nodejs.org/
3. Run in *Command Prompt*: `npm install -g crunchyroll`
## Instructions
Use the applicable instructions for the interface of your choice (currently limited to command-line).
### Command-line Interface (`crunchyroll`)
The [command-line interface](http://en.wikipedia.org/wiki/Command-line_interface) does not have a graphical component and is ideal for automation purposes and headless machines. The interface can run using a sequence of series addresses (the site address containing the episode listing), or with a batch-mode source file. The `crunchyroll --help` command will produce the following output:
Usage: crunchyroll [options]
Options:
-h, --help output usage information
-V, --version output the version number
-p, --pass <s> The password.
-u, --user <s> The e-mail address or username.
-c, --cache Disables the cache.
-m, --merge Disables merging subtitles and videos.
-e, --episode <i> The episode filter.
-v, --volume <i> The volume filter.
-f, --format <s> The subtitle format. (Default: ass)
-o, --output <s> The output path.
-s, --series <s> The series override.
-t, --tag <s> The subgroup. (Default: CrunchyRoll)
#### Batch-mode
When no sequence of series addresses is provided, the batch-mode source file will be read (which is *CrunchyRoll.txt* in the current work directory. Each line in this file is processed as a seperate command-line statement. This makes it ideal to manage a large sequence of series addresses with variating command-line options or incremental episode updates.
#### Examples
Download in batch-mode:
crunchyroll
Download *Fairy Tail* to the current work directory:
crunchyroll http://www.crunchyroll.com/fairy-tail
Download *Fairy Tail* to `C:\Anime`:
crunchyroll --output C:\Anime http://www.crunchyroll.com/fairy-tail
#### Switches
##### Authentication
* `-p or --pass <s>` sets the password.
* `-u or --user <s>` sets the e-mail address or username.
##### Disables
* `-c or --cache` disables the cache.
* `-m or --merge` disables merging subtitles and videos.
##### Filters
* `-e or --episode <i>` filters episodes (positive is greater than, negative is smaller than).
* `-v or --volume <i>` filters volumes (positive is greater than, negative is smaller than).
##### Settings
* `-f or --format <s>` sets the subtitle format. (Default: ass)
* `-o or --output <s>` sets the output path.
* `-s or --series <s>` sets the series override.
* `-t or --tag <s>` sets The subgroup. (Default: CrunchyRoll)
## Developers ## Developers
* Visual Studio 2013 Update 4 (Core) More information will be added at a later point. For now the recommendations are:
* NodeJS Tools (Debugging)
* TypeScript 1.4 (Language)
* ReSharper 9.0+ (Hints/Formatting)
* Web Essentials (TSLint)
## Work In Progress * Atom with `atom-typescript` and `linter-tslint` (and dependencies).
Open an issue or e-mail me directly. I'd be happy to answer your questions. Since this project uses TypeScript, compile with `node ts` or `npm install`.

BIN
bin/ffmpeg.exe Executable file

Binary file not shown.

View File

@@ -1,102 +0,0 @@
<?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>

View File

@@ -1,22 +0,0 @@

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

@@ -1,65 +0,0 @@
<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/CodeEditing/Intellisense/CodeCompletion/IntellisenseGloballyEnabled/IntellisenseEnabled/@EntryValue">Disabled</s:String>
<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:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FHTML_005FCONTROL/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FTAG_005FNAME/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FTAG_005FPREFIX/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=NAMESPACE_005FALIAS/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" 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

@@ -11,26 +11,26 @@
"type": "git", "type": "git",
"url": "git://github.com/Deathspike/crunchyroll.js.git" "url": "git://github.com/Deathspike/crunchyroll.js.git"
}, },
"version": "1.1.0", "version": "1.1.5",
"bin": { "bin": {
"crunchyroll": "./bin/crunchyroll" "crunchyroll": "./bin/crunchyroll"
}, },
"dependencies": { "dependencies": {
"big-integer": "^1.4.3", "big-integer": "1.4.4",
"cheerio": "^0.18.0", "cheerio": "0.18.0",
"commander": "^2.6.0", "commander": "2.6.0",
"mkdirp": "^0.5.0", "mkdirp": "0.5.0",
"request": "^2.53.0", "request": "2.53.0",
"xml2js": "^0.4.4" "xml2js": "0.4.5"
}, },
"devDependencies": { "devDependencies": {
"tsd": "^0.5.7", "tsd": "0.5.7",
"tslint": "^2.1.0", "tslint": "2.3.0-beta",
"typescript": "^1.4.1" "typescript": "1.5.0-beta"
}, },
"scripts": { "scripts": {
"prepublish": "npm run tsd && node ts", "prepublish": "npm run tsd && tsc",
"test": "node ts --only-test", "test": "node ts --only-test",
"tsd": "./node_modules/.bin/tsd reinstall --overwrite" "tsd": "tsd reinstall -o -s"
} }
} }

View File

@@ -1,15 +1,13 @@
'use strict'; 'use strict';
export = main;
import commander = require('commander'); import commander = require('commander');
import fs = require('fs'); import fs = require('fs');
import path = require('path'); import path = require('path');
import series = require('./series'); import series from './series';
import typings = require('./typings');
/** /**
* Streams the batch of series to disk. * Streams the batch of series to disk.
*/ */
function main(args: string[], done: (err?: Error) => void) { export default function(args: string[], done: (err?: Error) => void) {
var config = parse(args); var config = parse(args);
var batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt'); var batchPath = path.join(config.output || process.cwd(), 'CrunchyRoll.txt');
tasks(config, batchPath, (err, tasks) => { tasks(config, batchPath, (err, tasks) => {
@@ -41,14 +39,15 @@ function split(value: string): string[] {
previous = i + 1; previous = i + 1;
} }
} }
pieces.push(value.substring(previous, i).match(/^"?(.+?)"?$/)[1]); var lastPiece = value.substring(previous, i).match(/^"?(.+?)"?$/);
if (lastPiece) pieces.push(lastPiece[1]);
return pieces; return pieces;
} }
/** /**
* Parses the configuration or reads the batch-mode file for tasks. * Parses the configuration or reads the batch-mode file for tasks.
*/ */
function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error, tasks?: typings.IConfigTask[]) => void) { function tasks(config: IConfigLine, batchPath: string, done: (err: Error, tasks?: IConfigTask[]) => void) {
if (config.args.length) { if (config.args.length) {
return done(null, config.args.map(address => { return done(null, config.args.map(address => {
return {address: address, config: config}; return {address: address, config: config};
@@ -58,7 +57,7 @@ function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error
if (!exists) return done(null, []); if (!exists) return done(null, []);
fs.readFile(batchPath, 'utf8', (err, data) => { fs.readFile(batchPath, 'utf8', (err, data) => {
if (err) return done(err); if (err) return done(err);
var map: typings.IConfigTask[] = []; var map: IConfigTask[] = [];
data.split(/\r?\n/).forEach(line => { data.split(/\r?\n/).forEach(line => {
if (/^(\/\/|#)/.test(line)) return; if (/^(\/\/|#)/.test(line)) return;
var lineConfig = parse(process.argv.concat(split(line))); var lineConfig = parse(process.argv.concat(split(line)));
@@ -75,7 +74,7 @@ function tasks(config: typings.IConfigLine, batchPath: string, done: (err: Error
/** /**
* Parses the arguments and returns a configuration. * Parses the arguments and returns a configuration.
*/ */
function parse(args: string[]): typings.IConfigLine { function parse(args: string[]): IConfigLine {
return new commander.Command().version(require('../package').version) return new commander.Command().version(require('../package').version)
// Authentication // Authentication
.option('-p, --pass <s>', 'The password.') .option('-p, --pass <s>', 'The password.')

View File

@@ -1,5 +1,5 @@
'use strict'; 'use strict';
import batch = require('./batch'); import batch from './batch';
batch(process.argv, (err: any) => { batch(process.argv, (err: any) => {
if (err) console.error(err.stack || err); if (err) console.error(err.stack || err);

View File

@@ -1,19 +1,17 @@
'use strict'; 'use strict';
export = main;
import cheerio = require('cheerio'); import cheerio = require('cheerio');
import fs = require('fs'); import fs = require('fs');
import mkdirp = require('mkdirp'); import mkdirp = require('mkdirp');
import request = require('./request'); import request = require('./request');
import path = require('path'); import path = require('path');
import subtitle = require('./subtitle/index'); import subtitle from './subtitle/index';
import typings = require('./typings'); import video from './video/index';
import video = require('./video/index');
import xml2js = require('xml2js'); import xml2js = require('xml2js');
/** /**
* Streams the episode to disk. * Streams the episode to disk.
*/ */
function main(config: typings.IConfig, address: string, done: (err: Error) => void) { export default function(config: IConfig, address: string, done: (err: Error) => void) {
scrapePage(config, address, (err, page) => { scrapePage(config, address, (err, page) => {
if (err) return done(err); if (err) return done(err);
scrapePlayer(config, address, page.id, (err, player) => { scrapePlayer(config, address, page.id, (err, player) => {
@@ -38,7 +36,7 @@ function complete(message: string, begin: number, done: (err: Error) => void) {
/** /**
* Downloads the subtitle and video. * Downloads the subtitle and video.
*/ */
function download(config: typings.IConfig, page: typings.IEpisodePage, player: typings.IEpisodePlayer, done: (err: Error) => void) { function download(config: IConfig, page: IEpisodePage, player: IEpisodePlayer, done: (err: Error) => void) {
var series = config.series || page.series; var series = config.series || page.series;
var fileName = name(config, page, series); var fileName = name(config, page, series);
var filePath = path.join(config.output || process.cwd(), series, fileName); var filePath = path.join(config.output || process.cwd(), series, fileName);
@@ -51,7 +49,8 @@ function download(config: typings.IConfig, page: typings.IEpisodePage, player: t
downloadVideo(config, page, player, filePath, err => { downloadVideo(config, page, player, filePath, err => {
if (err) return done(err); if (err) return done(err);
if (config.merge) return complete('Finished ' + fileName, now, done); if (config.merge) return complete('Finished ' + fileName, now, done);
video.merge(config, player.video.file, filePath, err => { var isSubtited = Boolean(player.subtitle);
video.merge(config, isSubtited, player.video.file, filePath, player.mode, err => {
if (err) return done(err); if (err) return done(err);
complete('Finished ' + fileName, now, done); complete('Finished ' + fileName, now, done);
}); });
@@ -63,8 +62,9 @@ function download(config: typings.IConfig, page: typings.IEpisodePage, player: t
/** /**
* Saves the subtitles to disk. * Saves the subtitles to disk.
*/ */
function downloadSubtitle(config: typings.IConfig, player: typings.IEpisodePlayer, filePath: string, done: (err: Error) => void) { function downloadSubtitle(config: IConfig, player: IEpisodePlayer, filePath: string, done: (err?: Error) => void) {
var enc = player.subtitle; var enc = player.subtitle;
if (!enc) return done();
subtitle.decode(enc.id, enc.iv, enc.data, (err, data) => { subtitle.decode(enc.id, enc.iv, enc.data, (err, data) => {
if (err) return done(err); if (err) return done(err);
var formats = subtitle.formats; var formats = subtitle.formats;
@@ -79,23 +79,24 @@ function downloadSubtitle(config: typings.IConfig, player: typings.IEpisodePlaye
/** /**
* Streams the video to disk. * Streams the video to disk.
*/ */
function downloadVideo(config: typings.IConfig, function downloadVideo(config: IConfig,
page: typings.IEpisodePage, page: IEpisodePage,
player: typings.IEpisodePlayer, player: IEpisodePlayer,
filePath: string, filePath: string,
done: (err: Error) => void) { done: (err: Error) => void) {
video.stream( video.stream(
player.video.host, player.video.host,
player.video.file, player.video.file,
page.swf, page.swf,
filePath + path.extname(player.video.file), filePath, path.extname(player.video.file),
player.video.mode,
done); done);
} }
/** /**
* Names the file based on the config, page, series and tag. * Names the file based on the config, page, series and tag.
*/ */
function name(config: typings.IConfig, page: typings.IEpisodePage, series: string) { function name(config: IConfig, page: IEpisodePage, series: string) {
var episode = (page.episode < 10 ? '0' : '') + page.episode; var episode = (page.episode < 10 ? '0' : '') + page.episode;
var volume = (page.volume < 10 ? '0' : '') + page.volume; var volume = (page.volume < 10 ? '0' : '') + page.volume;
var tag = config.tag || 'CrunchyRoll'; var tag = config.tag || 'CrunchyRoll';
@@ -114,14 +115,14 @@ function prefix(value: number|string, length: number) {
/** /**
* Requests the page data and scrapes the id, episode, series and swf. * 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) { function scrapePage(config: IConfig, address: string, done: (err: Error, page?: IEpisodePage) => void) {
var id = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10); var id = parseInt((address.match(/[0-9]+$/) || ['0'])[0], 10);
if (!id) return done(new Error('Invalid address.')); if (!id) return done(new Error('Invalid address.'));
request.get(config, address, (err, result) => { request.get(config, address, (err, result) => {
if (err) return done(err); if (err) return done(err);
var $ = cheerio.load(result); var $ = cheerio.load(result);
var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href')); var swf = /^([^?]+)/.exec($('link[rel=video_src]').attr('href'));
var regexp = /Watch\s+(.+?)(?:\s+Season\s+([0-9]+))?\s+Episode\s+([0-9]+)/; var regexp = /-\s+(?:Watch\s+)?(.+?)(?:\s+Season\s+([0-9]+))?(?:\s+-)?\s+Episode\s+([0-9]+)/;
var data = regexp.exec($('title').text()); var data = regexp.exec($('title').text());
if (!swf || !data) return done(new Error('Invalid page.')); if (!swf || !data) return done(new Error('Invalid page.'));
done(null, { done(null, {
@@ -137,7 +138,7 @@ function scrapePage(config: typings.IConfig, address: string, done: (err: Error,
/** /**
* Requests the player data and scrapes the subtitle and video data. * 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) { function scrapePlayer(config: IConfig, address: string, id: number, done: (err: Error, player?: IEpisodePlayer) => void) {
var url = address.match(/^(https?:\/\/[^\/]+)/); var url = address.match(/^(https?:\/\/[^\/]+)/);
if (!url) return done(new Error('Invalid address.')); if (!url) return done(new Error('Invalid address.'));
request.post(config, { request.post(config, {
@@ -148,16 +149,23 @@ function scrapePlayer(config: typings.IConfig, address: string, id: number, done
xml2js.parseString(result, { xml2js.parseString(result, {
explicitArray: false, explicitArray: false,
explicitRoot: false explicitRoot: false
}, (err: Error, player: typings.IEpisodePlayerConfig) => { }, (err: Error, player: IEpisodePlayerConfig) => {
if (err) return done(err); if (err) return done(err);
try { try {
var isSubtitled = Boolean(player['default:preload'].subtitle);
var streamMode="RTMP";
if (player['default:preload'].stream_info.host == "")
{
streamMode="HLS";
}
done(null, { done(null, {
subtitle: { subtitle: isSubtitled ? {
id: parseInt(player['default:preload'].subtitle.$.id, 10), id: parseInt(player['default:preload'].subtitle.$.id, 10),
iv: player['default:preload'].subtitle.iv, iv: player['default:preload'].subtitle.iv,
data: player['default:preload'].subtitle.data data: player['default:preload'].subtitle.data
}, } : null,
video: { video: {
mode: streamMode;
file: player['default:preload'].stream_info.file, file: player['default:preload'].stream_info.file,
host: player['default:preload'].stream_info.host host: player['default:preload'].stream_info.host
} }

View File

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

16
src/interface/IConfig.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
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;
}

3
src/interface/IConfigLine.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
interface IConfigLine extends IConfig {
args: string[];
}

4
src/interface/IConfigTask.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
interface IConfigTask {
address: string;
config: IConfigLine;
}

7
src/interface/IEpisodePage.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
interface IEpisodePage {
id: number;
episode: number;
series: string;
volume: number;
swf: string;
}

11
src/interface/IEpisodePlayer.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
interface IEpisodePlayer {
subtitle?: {
id: number;
iv: string;
data: string;
};
video: {
file: string;
host: string;
};
}

15
src/interface/IEpisodePlayerConfig.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
interface IEpisodePlayerConfig {
'default:preload': {
subtitle: {
$: {
id: string;
};
iv: string;
data: string;
};
stream_info: {
file: string;
host: string;
};
};
}

3
src/interface/IFormatterTable.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
interface IFormatterTable {
[key: string]: (input: string|Buffer, done: (err: Error, subtitle?: string) => void) => void;
}

4
src/interface/ISeries.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
interface ISeries {
episodes: ISeriesEpisode[];
series: string;
}

5
src/interface/ISeriesEpisode.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
interface ISeriesEpisode {
address: string;
episode: number;
volume: number;
}

13
src/interface/ISubtitle.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
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;
}

15
src/interface/ISubtitleEvent.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
interface ISubtitleEvent {
event: {
$: {
end: string;
start: string;
style: string;
name: string;
margin_l: string;
margin_r: string;
margin_v: string;
effect: string;
text: string;
};
}[];
}

29
src/interface/ISubtitleStyle.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
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,12 +1,11 @@
'use strict'; 'use strict';
import request = require('request'); import request = require('request');
import typings = require('./typings');
var isAuthenticated = false; var isAuthenticated = false;
/** /**
* Performs a GET request for the resource. * Performs a GET request for the resource.
*/ */
export function get(config: typings.IConfig, options: request.Options, done: (err: Error, result?: string) => void) { export function get(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
authenticate(config, err => { authenticate(config, err => {
if (err) return done(err); if (err) return done(err);
request.get(modify(options), (err: Error, response: any, body: any) => { request.get(modify(options), (err: Error, response: any, body: any) => {
@@ -19,7 +18,7 @@ export function get(config: typings.IConfig, options: request.Options, done: (er
/** /**
* Performs a POST request for the resource. * Performs a POST request for the resource.
*/ */
export function post(config: typings.IConfig, options: request.Options, done: (err: Error, result?: string) => void) { export function post(config: IConfig, options: request.Options, done: (err: Error, result?: string) => void) {
authenticate(config, err => { authenticate(config, err => {
if (err) return done(err); if (err) return done(err);
request.post(modify(options), (err: Error, response: any, body: any) => { request.post(modify(options), (err: Error, response: any, body: any) => {
@@ -32,7 +31,7 @@ export function post(config: typings.IConfig, options: request.Options, done: (e
/** /**
* Authenticates using the configured pass and user. * Authenticates using the configured pass and user.
*/ */
function authenticate(config: typings.IConfig, done: (err: Error) => void) { function authenticate(config: IConfig, done: (err: Error) => void) {
if (isAuthenticated || !config.pass || !config.user) return done(null); if (isAuthenticated || !config.pass || !config.user) return done(null);
var options = { var options = {
form: { form: {

View File

@@ -1,18 +1,16 @@
'use strict'; 'use strict';
export = main;
import cheerio = require('cheerio'); import cheerio = require('cheerio');
import episode = require('./episode'); import episode from './episode';
import fs = require('fs'); import fs = require('fs');
import request = require('./request'); import request = require('./request');
import path = require('path'); import path = require('path');
import typings = require('./typings');
import url = require('url'); import url = require('url');
var persistent = '.crpersistent'; var persistent = '.crpersistent';
/** /**
* Streams the series to disk. * Streams the series to disk.
*/ */
function main(config: typings.IConfig, address: string, done: (err: Error) => void) { export default function(config: IConfig, address: string, done: (err: Error) => void) {
var persistentPath = path.join(config.output || process.cwd(), persistent); var persistentPath = path.join(config.output || process.cwd(), persistent);
fs.readFile(persistentPath, 'utf8', (err, contents) => { fs.readFile(persistentPath, 'utf8', (err, contents) => {
var cache = config.cache ? {} : JSON.parse(contents || '{}'); var cache = config.cache ? {} : JSON.parse(contents || '{}');
@@ -39,9 +37,9 @@ function main(config: typings.IConfig, address: string, done: (err: Error) => vo
* Downloads the episode. * Downloads the episode.
*/ */
function download(cache: {[address: string]: number}, function download(cache: {[address: string]: number},
config: typings.IConfig, config: IConfig,
baseAddress: string, baseAddress: string,
item: typings.ISeriesEpisode, item: ISeriesEpisode,
done: (err: Error) => void) { done: (err: Error) => void) {
if (!filter(config, item)) return done(null); if (!filter(config, item)) return done(null);
var address = url.resolve(baseAddress, item.address); var address = url.resolve(baseAddress, item.address);
@@ -56,7 +54,7 @@ function download(cache: {[address: string]: number},
/** /**
* Filters the item based on the configuration. * Filters the item based on the configuration.
*/ */
function filter(config: typings.IConfig, item: typings.ISeriesEpisode) { function filter(config: IConfig, item: ISeriesEpisode) {
// Filter on chapter. // Filter on chapter.
var episodeFilter = config.episode; var episodeFilter = config.episode;
if (episodeFilter > 0 && item.episode <= episodeFilter) return false; if (episodeFilter > 0 && item.episode <= episodeFilter) return false;
@@ -72,13 +70,13 @@ function filter(config: typings.IConfig, item: typings.ISeriesEpisode) {
/** /**
* Requests the page and scrapes the episodes and series. * Requests the page and scrapes the episodes and series.
*/ */
function page(config: typings.IConfig, address: string, done: (err: Error, result?: typings.ISeries) => void) { function page(config: IConfig, address: string, done: (err: Error, result?: ISeries) => void) {
request.get(config, address, (err, result) => { request.get(config, address, (err, result) => {
if (err) return done(err); if (err) return done(err);
var $ = cheerio.load(result); var $ = cheerio.load(result);
var title = $('span[itemprop=name]').text(); var title = $('span[itemprop=name]').text();
if (!title) return done(new Error('Invalid page.')); if (!title) return done(new Error('Invalid page.'));
var episodes: typings.ISeriesEpisode[] = []; var episodes: ISeriesEpisode[] = [];
$('.episode').each((i, el) => { $('.episode').each((i, el) => {
if ($(el).children('img[src*=coming_soon]').length) return; if ($(el).children('img[src*=coming_soon]').length) return;
var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text()); var volume = /([0-9]+)\s*$/.exec($(el).closest('ul').prev('a').text());

View File

@@ -1,6 +1,5 @@
/* tslint:disable:no-bitwise false */ /* tslint:disable:no-bitwise false */
'use strict'; 'use strict';
export = main;
import crypto = require('crypto'); import crypto = require('crypto');
import bigInt = require('big-integer'); import bigInt = require('big-integer');
import zlib = require('zlib'); import zlib = require('zlib');
@@ -8,7 +7,7 @@ import zlib = require('zlib');
/** /**
* Decodes the data. * Decodes the data.
*/ */
function main(id: number, iv: Buffer|string, data: Buffer|string, done: (err?: Error, result?: Buffer) => void) { export default function(id: number, iv: Buffer|string, data: Buffer|string, done: (err?: Error, result?: Buffer) => void) {
try { try {
decompress(decrypt(id, iv, data), done); decompress(decrypt(id, iv, data), done);
} catch (e) { } catch (e) {

View File

@@ -1,16 +1,14 @@
'use strict'; 'use strict';
export = main;
import xml2js = require('xml2js'); import xml2js = require('xml2js');
import typings = require('../../typings');
/** /**
* Converts an input buffer to a SubStation Alpha subtitle. * Converts an input buffer to a SubStation Alpha subtitle.
*/ */
function main(input: string|Buffer, done: (err: Error, subtitle?: string) => void) { export default function(input: string|Buffer, done: (err: Error, subtitle?: string) => void) {
xml2js.parseString(input.toString(), { xml2js.parseString(input.toString(), {
explicitArray: false, explicitArray: false,
explicitRoot: false explicitRoot: false
}, (err: Error, xml: typings.ISubtitle) => { }, (err: Error, xml: ISubtitle) => {
if (err) return done(err); if (err) return done(err);
try { try {
done(null, script(xml) + '\n' + done(null, script(xml) + '\n' +
@@ -25,11 +23,11 @@ function main(input: string|Buffer, done: (err: Error, subtitle?: string) => voi
/** /**
* Converts the event block. * Converts the event block.
*/ */
function event(block: typings.ISubtitleEvent): string { function event(block: ISubtitleEvent): string {
var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'; var format = 'Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
return '[Events]\n' + return '[Events]\n' +
'Format: ' + format + '\n' + 'Format: ' + format + '\n' +
block.event.map(style => ('Dialogue: 0,' + [].concat(block.event).map(style => ('Dialogue: 0,' +
style.$.start + ',' + style.$.start + ',' +
style.$.end + ',' + style.$.end + ',' +
style.$.style + ',' + style.$.style + ',' +
@@ -44,7 +42,7 @@ function event(block: typings.ISubtitleEvent): string {
/** /**
* Converts the script block. * Converts the script block.
*/ */
function script(block: typings.ISubtitle): string { function script(block: ISubtitle): string {
return '[Script Info]\n' + return '[Script Info]\n' +
'Title: ' + block.$.title + '\n' + 'Title: ' + block.$.title + '\n' +
'ScriptType: v4.00+\n' + 'ScriptType: v4.00+\n' +
@@ -59,14 +57,14 @@ function script(block: typings.ISubtitle): string {
/** /**
* Converts the style block. * Converts the style block.
*/ */
function style(block: typings.ISubtitleStyle): string { function style(block: ISubtitleStyle): string {
var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' + var format = 'Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,' +
'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' + 'OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,' +
'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' + 'ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,' +
'MarginL,MarginR,MarginV,Encoding'; 'MarginL,MarginR,MarginV,Encoding';
return '[V4+ Styles]\n' + return '[V4+ Styles]\n' +
'Format: ' + format + '\n' + 'Format: ' + format + '\n' +
block.style.map(style => 'Style: ' + [].concat(block.style).map(style => 'Style: ' +
style.$.name + ',' + style.$.name + ',' +
style.$.font_name + ',' + style.$.font_name + ',' +
style.$.font_size + ',' + style.$.font_size + ',' +

View File

@@ -1,10 +1,8 @@
'use strict'; 'use strict';
export = main; import ass from './ass';
import ass = require('./ass'); import srt from './srt';
import srt = require('./srt');
import typings = require('../../typings');
var main: typings.IFormatterTable = { export default <IFormatterTable> {
ass: ass, ass: ass,
srt: srt srt: srt
}; };

View File

@@ -1,14 +1,12 @@
'use strict'; 'use strict';
export = srt;
import xml2js = require('xml2js'); import xml2js = require('xml2js');
import typings = require('../../typings');
/** /**
* Converts an input buffer to a SubRip subtitle. * Converts an input buffer to a SubRip subtitle.
*/ */
function srt(input: Buffer|string, done: (err: Error, subtitle?: string) => void) { export default function(input: Buffer|string, done: (err: Error, subtitle?: string) => void) {
var options = {explicitArray: false, explicitRoot: false}; var options = {explicitArray: false, explicitRoot: false};
xml2js.parseString(input.toString(), options, (err: Error, xml: typings.ISubtitle) => { xml2js.parseString(input.toString(), options, (err: Error, xml: ISubtitle) => {
try { try {
if (err) return done(err); if (err) return done(err);
done(null, xml.events.event.map((event, index) => { done(null, xml.events.event.map((event, index) => {

View File

@@ -1,3 +1,4 @@
'use strict'; 'use strict';
export import decode = require('./decode'); import decode from './decode';
export import formats = require('./formats/index'); import formats from './formats/index';
export default {decode, formats};

View File

@@ -1,136 +0,0 @@
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,3 +1,4 @@
'use strict'; 'use strict';
export import merge = require('./merge'); import merge from './merge';
export import stream = require('./stream'); import stream from './stream';
export default {merge, stream};

View File

@@ -1,22 +1,28 @@
'use strict'; 'use strict';
export = main;
import childProcess = require('child_process'); import childProcess = require('child_process');
import fs = require('fs'); import fs = require('fs');
import path = require('path'); import path = require('path');
import os = require('os'); import os = require('os');
import subtitle = require('../subtitle/index'); import subtitle from '../subtitle/index';
import typings = require('../typings');
/** /**
* Merges the subtitle and video files into a Matroska Multimedia Container. * Merges the subtitle and video files into a Matroska Multimedia Container.
*/ */
function main(config: typings.IConfig, rtmpInputPath: string, filePath: string, done: (err: Error) => void) { export default function(config: IConfig, isSubtitled: boolean, rtmpInputPath: string, filePath: string, streamMode: string, done: (err: Error) => void) {
var subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass'); var subtitlePath = filePath + '.' + (subtitle.formats[config.format] ? config.format : 'ass');
var videoPath = filePath + path.extname(rtmpInputPath); var videoPath = filePath;
if (streamMode == "RTMP")
{
videoPath += path.extname(rtmpInputPath);
}
else
{
videoPath += ".mp4";
}
childProcess.exec(command() + ' ' + childProcess.exec(command() + ' ' +
'-o "' + filePath + '.mkv" ' + '-o "' + filePath + '.mkv" ' +
'"' + videoPath + '" ' + '"' + videoPath + '" ' +
'"' + subtitlePath + '"', { (isSubtitled ? '"' + subtitlePath + '"' : ''), {
maxBuffer: Infinity maxBuffer: Infinity
}, err => { }, err => {
if (err) return done(err); if (err) return done(err);
@@ -32,7 +38,7 @@ function main(config: typings.IConfig, rtmpInputPath: string, filePath: string,
*/ */
function command(): string { function command(): string {
if (os.platform() !== 'win32') return 'mkvmerge'; if (os.platform() !== 'win32') return 'mkvmerge';
return path.join(__dirname, '../../bin/mkvmerge.exe'); return '"' + path.join(__dirname, '../../bin/mkvmerge.exe') + '"';
} }
/** /**

View File

@@ -1,5 +1,4 @@
'use strict'; 'use strict';
export = main;
import childProcess = require('child_process'); import childProcess = require('child_process');
import path = require('path'); import path = require('path');
import os = require('os'); import os = require('os');
@@ -7,20 +6,38 @@ import os = require('os');
/** /**
* Streams the video to disk. * Streams the video to disk.
*/ */
function main(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string, done: (err: Error) => void) { export default function(rtmpUrl: string, rtmpInputPath: string, swfUrl: string, filePath: string, fileExt: string, mode: string, done: (err: Error) => void) {
childProcess.exec(command() + ' ' + if (mode == "RTMP")
'-r "' + rtmpUrl + '" ' + {
'-y "' + rtmpInputPath + '" ' + childProcess.exec(command("rtmpdump") + ' ' +
'-W "' + swfUrl + '" ' + '-r "' + rtmpUrl + '" ' +
'-o "' + filePath + '"', { '-y "' + rtmpInputPath + '" ' +
maxBuffer: Infinity '-W "' + swfUrl + '" ' +
}, done); '-o "' + filePath + fileExt + '"', {
maxBuffer: Infinity
}, done);
}
else if (mode == "HLS")
{
console.info("Experimental FFMPEG, MAY FAIL!!!");
var cmd=command("ffmpeg") + ' ' +
'-i "' + rtmpInputPath + '" ' +
'-c copy -bsf:a aac_adtstoasc ' +
'"' + filePath + '.mp4"';
childProcess.exec(cmd, {
maxBuffer: Infinity
}, done);
}
else
{
console.error("No such mode: " + mode);
}
} }
/** /**
* Determines the command for the operating system. * Determines the command for the operating system.
*/ */
function command(): string { function command(exe: string): string {
if (os.platform() !== 'win32') return 'rtmpdump'; if (os.platform() !== 'win32') return exe;
return path.join(__dirname, '../../bin/rtmpdump.exe'); return '"' + path.join(__dirname, '../../bin/' + exe + '.exe') + '"';
} }

26
ts.js
View File

@@ -4,6 +4,15 @@ var fs = require('fs');
var path = require('path'); var path = require('path');
var isTest = process.argv[2] === '--only-test'; var isTest = process.argv[2] === '--only-test';
// TODO: This build task should be removed upon release of TypeScript 1.5 with
// the support for `tsconfig.json`. Invoking `tsc` from `package.json` will then
// read the configuration and compile accordingly. It seems that `TSLint` will,
// eventually, support this mechanism too. That prevents the need for any kind
// of build task and will run entirely based on instructions from `npm`.
//
// Reference #1: https://github.com/Microsoft/TypeScript/issues/1667
// Reference #2: https://github.com/palantir/tslint/issues/281
read(function(err, fileNames) { read(function(err, fileNames) {
clean(fileNames, function() { clean(fileNames, function() {
var hasLintError = false; var hasLintError = false;
@@ -48,13 +57,13 @@ function clean(filePaths, done) {
* @param {function(Error)} done * @param {function(Error)} done
*/ */
function compile(filePaths, done) { function compile(filePaths, done) {
if (isTest) return done(null); if (isTest) return done(null);
var execPath = path.join(__dirname, 'node_modules/.bin/tsc'); var execPath = path.join(__dirname, 'node_modules/.bin/tsc');
var options = '--declaration --module CommonJS --noImplicitAny --outDir dist'; var options = '--declaration --module CommonJS --noImplicitAny --outDir dist --target ES5';
childProcess.exec([execPath, options].concat(filePaths).join(' '), function(err, stdout) { childProcess.exec([execPath, options].concat(filePaths).join(' '), function(err, stdout) {
if (stdout) return done(new Error(stdout)); if (stdout) return done(new Error(stdout));
done(null); done(null);
}); });
} }
/** /**
@@ -83,10 +92,5 @@ function lint(filePaths, handler, done) {
* @param {function(Error, Array.<string>)} done * @param {function(Error, Array.<string>)} done
*/ */
function read(done) { function read(done) {
var contents = fs.readFileSync('crunchyroll.js.njsproj', 'utf8'); done(null, JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')).files);
var expression = /<TypeScriptCompile\s+Include="([\w\W]+?\.ts)" \/>/g;
var matches;
var filePaths = [];
while ((matches = expression.exec(contents))) filePaths.push(matches[1]);
done(null, filePaths);
} }

52
tsconfig.json Normal file
View File

@@ -0,0 +1,52 @@
{
"version": "1.5.1-beta",
"compilerOptions": {
"declaration": true,
"noImplicitAny": true,
"removeComments": false,
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"target": "es5"
},
"filesGlob": [
"src/**/*.ts",
"typings/**/*.ts"
],
"files": [
"src/batch.ts",
"src/cli.ts",
"src/episode.ts",
"src/index.ts",
"src/interface/IConfig.d.ts",
"src/interface/IConfigLine.d.ts",
"src/interface/IConfigTask.d.ts",
"src/interface/IEpisodePage.d.ts",
"src/interface/IEpisodePlayer.d.ts",
"src/interface/IEpisodePlayerConfig.d.ts",
"src/interface/IFormatterTable.d.ts",
"src/interface/ISeries.d.ts",
"src/interface/ISeriesEpisode.d.ts",
"src/interface/ISubtitle.d.ts",
"src/interface/ISubtitleEvent.d.ts",
"src/interface/ISubtitleStyle.d.ts",
"src/request.ts",
"src/series.ts",
"src/subtitle/decode.ts",
"src/subtitle/formats/ass.ts",
"src/subtitle/formats/index.ts",
"src/subtitle/formats/srt.ts",
"src/subtitle/index.ts",
"src/video/index.ts",
"src/video/merge.ts",
"src/video/stream.ts",
"typings/big-integer/big-integer.d.ts",
"typings/cheerio/cheerio.d.ts",
"typings/commander/commander.d.ts",
"typings/form-data/form-data.d.ts",
"typings/mkdirp/mkdirp.d.ts",
"typings/node/node.d.ts",
"typings/request/request.d.ts",
"typings/xml2js/xml2js.d.ts"
]
}

View File

@@ -6,28 +6,28 @@
"bundle": "typings/tsd.d.ts", "bundle": "typings/tsd.d.ts",
"installed": { "installed": {
"node/node.d.ts": { "node/node.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}, },
"commander/commander.d.ts": { "commander/commander.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}, },
"xml2js/xml2js.d.ts": { "xml2js/xml2js.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}, },
"cheerio/cheerio.d.ts": { "cheerio/cheerio.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}, },
"mkdirp/mkdirp.d.ts": { "mkdirp/mkdirp.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}, },
"request/request.d.ts": { "request/request.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}, },
"big-integer/big-integer.d.ts": { "big-integer/big-integer.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
}, },
"form-data/form-data.d.ts": { "form-data/form-data.d.ts": {
"commit": "42c8a3b74c05f6887ce21dd63c6234e424f9f8fe" "commit": "3882d337bb0808cde9fe4c08012508a48c135482"
} }
} }
} }