├── global.json ├── src ├── YoutubeExtractorCore │ ├── AdaptiveType.cs │ ├── AudioType.cs │ ├── VideoType.cs │ ├── YoutubeVideoNotAvailableException.cs │ ├── YoutubeParseException.cs │ ├── ProgressEventArgs.cs │ ├── project.json │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── YoutubeExtractorCore.xproj │ ├── IAudioExtractor.cs │ ├── AudioExtractionException.cs │ ├── HttpHelper.cs │ ├── Downloader.cs │ ├── BigEndianBitConverter.cs │ ├── VideoDownloader.cs │ ├── BitHelper.cs │ ├── AacAudioExtractor.cs │ ├── AudioDownloader.cs │ ├── Decipherer.cs │ ├── FlvFile.cs │ ├── VideoInfo.cs │ ├── Mp3AudioExtractor.cs │ └── DownloadUrlResolver.cs └── YoutubeExtractorCore.Example │ ├── project.json │ ├── Properties │ ├── PublishProfiles │ │ ├── wut-publish.ps1 │ │ ├── wut.pubxml │ │ └── publish-module.psm1 │ └── AssemblyInfo.cs │ ├── YoutubeExtractorCore.Example.xproj │ └── Program.cs ├── README.md ├── YoutubeExtractorCore.sln ├── .gitattributes └── .gitignore /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ], 3 | "sdk": { 4 | "version": "1.0.0-preview2-003121" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/AdaptiveType.cs: -------------------------------------------------------------------------------- 1 | namespace YoutubeExtractorCore 2 | { 3 | public enum AdaptiveType 4 | { 5 | None, 6 | Audio, 7 | Video 8 | } 9 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/AudioType.cs: -------------------------------------------------------------------------------- 1 | namespace YoutubeExtractorCore 2 | { 3 | public enum AudioType 4 | { 5 | Aac, 6 | Mp3, 7 | Vorbis, 8 | 9 | /// 10 | /// The audio type is unknown. This can occur if YoutubeExtractor is not up-to-date. 11 | /// 12 | Unknown 13 | } 14 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore.Example/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "emitEntryPoint": true 5 | }, 6 | 7 | "dependencies": { 8 | "Microsoft.NETCore.Runtime.CoreCLR": "1.0.2", 9 | "System.Console": "4.0.0", 10 | "System.IO.Compression": "4.1.0", 11 | "YoutubeExtractorCore": "0.1.0" 12 | }, 13 | 14 | "frameworks": { 15 | "netcoreapp1.0": { 16 | "imports": "dnxcore50" 17 | } 18 | }, 19 | "runtimes": { 20 | "win10-x86": {}, 21 | "win10-x64": {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/VideoType.cs: -------------------------------------------------------------------------------- 1 | namespace YoutubeExtractorCore 2 | { 3 | /// 4 | /// The video type. Also known as video container. 5 | /// 6 | public enum VideoType 7 | { 8 | /// 9 | /// Video for mobile devices (3GP). 10 | /// 11 | Mobile, 12 | 13 | Flash, 14 | Mp4, 15 | WebM, 16 | 17 | /// 18 | /// The video type is unknown. This can occur if YoutubeExtractor is not up-to-date. 19 | /// 20 | Unknown 21 | } 22 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/YoutubeVideoNotAvailableException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace YoutubeExtractorCore 4 | { 5 | /// 6 | /// The exception that is thrown when the video is not available for viewing. 7 | /// This can happen when the uploader restricts the video to specific countries. 8 | /// 9 | public class YoutubeVideoNotAvailableException : Exception 10 | { 11 | public YoutubeVideoNotAvailableException() 12 | { } 13 | 14 | public YoutubeVideoNotAvailableException(string message) 15 | : base(message) 16 | { } 17 | } 18 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/YoutubeParseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace YoutubeExtractorCore 4 | { 5 | /// 6 | /// 7 | /// The exception that is thrown when the YouTube page could not be parsed. 8 | /// This happens, when YouTube changes the structure of their page. 9 | /// 10 | /// Please report when this exception happens at www.github.com/flagbug/YoutubeExtractor/issues 11 | /// 12 | public class YoutubeParseException : Exception 13 | { 14 | public YoutubeParseException(string message, Exception innerException) 15 | : base(message, innerException) 16 | { } 17 | } 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

YoutubeExtractorCore

2 |

Overview

3 |

.NET Core library used for extracting information about Youtube videos based on flatbug/YoutubeExtractor

4 |

Ported to be usable with .NET Core 1.0, added async support and better exception handling

5 |

NuGet

6 |

Available as NuGet package

7 | Install-Package YoutubeExtractorCore 8 |

Licence

9 |

The library is licenced under the MIT Licence

10 |

Example code

11 |

Library comes with simple example app, more examples can be found on the original project site

12 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/ProgressEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace YoutubeExtractorCore 7 | { 8 | public class ProgressEventArgs : EventArgs 9 | { 10 | public ProgressEventArgs(double progressPercentage) 11 | { 12 | this.ProgressPercentage = progressPercentage; 13 | } 14 | 15 | /// 16 | /// Gets or sets a token whether the operation that reports the progress should be canceled. 17 | /// 18 | public bool Cancel { get; set; } 19 | 20 | /// 21 | /// Gets the progress percentage in a range from 0.0 to 100.0. 22 | /// 23 | public double ProgressPercentage { get; private set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore.Example/Properties/PublishProfiles/wut-publish.ps1: -------------------------------------------------------------------------------- 1 | [cmdletbinding(SupportsShouldProcess=$true)] 2 | param($publishProperties=@{}, $packOutput, $pubProfilePath) 3 | 4 | # to learn more about this file visit https://go.microsoft.com/fwlink/?LinkId=524327 5 | 6 | try{ 7 | if ($publishProperties['ProjectGuid'] -eq $null){ 8 | $publishProperties['ProjectGuid'] = 'feab531c-9da7-4fd6-bdd8-3e6ea3791b60' 9 | } 10 | 11 | $publishModulePath = Join-Path (Split-Path $MyInvocation.MyCommand.Path) 'publish-module.psm1' 12 | Import-Module $publishModulePath -DisableNameChecking -Force 13 | 14 | # call Publish-AspNet to perform the publish operation 15 | Publish-AspNet -publishProperties $publishProperties -packOutput $packOutput -pubProfilePath $pubProfilePath 16 | } 17 | catch{ 18 | "An error occurred during publish.`n{0}" -f $_.Exception.Message | Write-Error 19 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "YoutubeExtractorCore", 3 | "description": ".NET Core library used for extracting information about Youtube videos", 4 | "version": "0.1.0", 5 | "authors": [ "Druchem" ], 6 | "packOptions": { 7 | "licenseUrl": "https://opensource.org/licenses/MIT", 8 | "projectUrl": "https://github.com/druchem/YoutubeExtractorCore", 9 | "tags": [ "Youtube", "Extractor", "Core" ] 10 | }, 11 | 12 | "dependencies": { 13 | "Microsoft.NETCore.Platforms": "1.1.0-preview1-24530-04", 14 | "Microsoft.Win32.Primitives": "4.0.1", 15 | "System.Net.Http": "4.1.0", 16 | "System.Runtime.InteropServices": "4.1.0", 17 | "Newtonsoft.Json": "9.0.1", 18 | "System.IO.FileSystem": "4.0.1", 19 | "System.Net.Requests": "4.0.11" 20 | }, 21 | 22 | "frameworks": { 23 | "netcoreapp1.0": { 24 | "imports": "dnxcore50" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("YoutubeExtractorCore")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("c861cbda-7fe5-4ab3-96e0-5529807cd696")] 20 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore.Example/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("YoutubeExtractorCore.Example")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("feab531c-9da7-4fd6-bdd8-3e6ea3791b60")] 20 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore.Example/Properties/PublishProfiles/wut.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | FileSystem 9 | Release 10 | Any CPU 11 | 12 | True 13 | False 14 | netcoreapp1.0 15 | True 16 | .\bin\Release\PublishOutput 17 | False 18 | 19 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/YoutubeExtractorCore.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | c861cbda-7fe5-4ab3-96e0-5529807cd696 11 | YoutubeExtractorCore 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore.Example/YoutubeExtractorCore.Example.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | feab531c-9da7-4fd6-bdd8-3e6ea3791b60 11 | YoutubeExtractorCore.Example 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/IAudioExtractor.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 20013-2014 Dennis Daume (daume.dennis@gmail.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | using System; 23 | 24 | namespace YoutubeExtractorCore 25 | { 26 | internal interface IAudioExtractor : IDisposable 27 | { 28 | string VideoPath { get; } 29 | 30 | /// An error occured while writing the chunk. 31 | void WriteChunk(byte[] chunk, uint timeStamp); 32 | } 33 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/AudioExtractionException.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 2013-2014 Dennis Daume (daume.dennis@gmail.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | using System; 23 | 24 | namespace YoutubeExtractorCore 25 | { 26 | /// 27 | /// The exception that is thrown when an error occurs durin audio extraction. 28 | /// 29 | public class AudioExtractionException : Exception 30 | { 31 | public AudioExtractionException(string message) 32 | : base(message) 33 | { } 34 | } 35 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace YoutubeExtractorCore.Example 6 | { 7 | public class Program 8 | { 9 | public static void Main() 10 | { 11 | RunExample().GetAwaiter().GetResult(); 12 | } 13 | 14 | private static async Task RunExample() 15 | { 16 | const string videoUrl = "https://www.youtube.com/watch?v=6tjHBbvKd3M"; 17 | try 18 | { 19 | var videoInfos = await DownloadUrlResolver.GetDownloadUrlsAsync(videoUrl, false); 20 | var videoWithAudio = 21 | videoInfos.FirstOrDefault(video => video.Resolution > 0 && video.AudioBitrate > 0); 22 | 23 | if (videoWithAudio != null) 24 | { 25 | Console.WriteLine($"Video title:{videoWithAudio.Title}"); 26 | Console.WriteLine($"Video url:{videoWithAudio.DownloadUrl}"); 27 | } 28 | else 29 | { 30 | Console.WriteLine("Video with audio not found"); 31 | } 32 | } 33 | catch (YoutubeVideoNotAvailableException) 34 | { 35 | Console.WriteLine("Video is not available"); 36 | } 37 | catch (YoutubeParseException) 38 | { 39 | Console.WriteLine("Error while trying to parse youtube data"); 40 | } 41 | catch (Exception ex) 42 | { 43 | Console.WriteLine(ex.Message); 44 | } 45 | 46 | Console.ReadLine(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /YoutubeExtractorCore.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8054A55A-49F2-499D-A841-C635315E0A73}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD19400B-3049-4592-A18B-1B8BF7629D46}" 9 | ProjectSection(SolutionItems) = preProject 10 | global.json = global.json 11 | EndProjectSection 12 | EndProject 13 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "YoutubeExtractorCore", "src\YoutubeExtractorCore\YoutubeExtractorCore.xproj", "{C861CBDA-7FE5-4AB3-96E0-5529807CD696}" 14 | EndProject 15 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "YoutubeExtractorCore.Example", "src\YoutubeExtractorCore.Example\YoutubeExtractorCore.Example.xproj", "{FEAB531C-9DA7-4FD6-BDD8-3E6EA3791B60}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {C861CBDA-7FE5-4AB3-96E0-5529807CD696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {C861CBDA-7FE5-4AB3-96E0-5529807CD696}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {C861CBDA-7FE5-4AB3-96E0-5529807CD696}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {C861CBDA-7FE5-4AB3-96E0-5529807CD696}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {FEAB531C-9DA7-4FD6-BDD8-3E6EA3791B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {FEAB531C-9DA7-4FD6-BDD8-3E6EA3791B60}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {FEAB531C-9DA7-4FD6-BDD8-3E6EA3791B60}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {FEAB531C-9DA7-4FD6-BDD8-3E6EA3791B60}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(NestedProjects) = preSolution 36 | {C861CBDA-7FE5-4AB3-96E0-5529807CD696} = {8054A55A-49F2-499D-A841-C635315E0A73} 37 | {FEAB531C-9DA7-4FD6-BDD8-3E6EA3791B60} = {8054A55A-49F2-499D-A841-C635315E0A73} 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/HttpHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | 9 | namespace YoutubeExtractorCore 10 | { 11 | internal static class HttpHelper 12 | { 13 | public static async Task DownloadStringAsync(string url) 14 | { 15 | using (var client = new HttpClient()) 16 | { 17 | return await client.GetStringAsync(url); 18 | } 19 | } 20 | 21 | public static IDictionary ParseQueryString(string s) 22 | { 23 | // remove anything other than query string from url 24 | if (s.Contains("?")) 25 | { 26 | s = s.Substring(s.IndexOf('?') + 1); 27 | } 28 | 29 | var dictionary = new Dictionary(); 30 | 31 | foreach (string vp in Regex.Split(s, "&")) 32 | { 33 | var strings = Regex.Split(vp, "="); 34 | dictionary.Add(strings[0], strings.Length == 2 ? UrlDecode(strings[1]) : string.Empty); 35 | } 36 | 37 | return dictionary; 38 | } 39 | 40 | public static string ReplaceQueryStringParameter(string currentPageUrl, string paramToReplace, string newValue) 41 | { 42 | var query = ParseQueryString(currentPageUrl); 43 | 44 | query[paramToReplace] = newValue; 45 | 46 | var resultQuery = new StringBuilder(); 47 | bool isFirst = true; 48 | 49 | foreach (var pair in query) 50 | { 51 | if (!isFirst) 52 | { 53 | resultQuery.Append("&"); 54 | } 55 | 56 | resultQuery.Append(pair.Key); 57 | resultQuery.Append("="); 58 | resultQuery.Append(pair.Value); 59 | 60 | isFirst = false; 61 | } 62 | 63 | var uriBuilder = new UriBuilder(currentPageUrl) 64 | { 65 | Query = resultQuery.ToString() 66 | }; 67 | 68 | return uriBuilder.ToString(); 69 | } 70 | 71 | public static string UrlDecode(string url) 72 | { 73 | return WebUtility.UrlDecode(url); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/Downloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace YoutubeExtractorCore 7 | { 8 | /// 9 | /// Provides the base class for the and class. 10 | /// 11 | public abstract class Downloader 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The video to download/convert. 17 | /// The path to save the video/audio. 18 | /// /// An optional value to limit the number of bytes to download. 19 | /// or is null. 20 | protected Downloader(VideoInfo video, string savePath, int? bytesToDownload = null) 21 | { 22 | if (video == null) 23 | throw new ArgumentNullException("video"); 24 | 25 | if (savePath == null) 26 | throw new ArgumentNullException("savePath"); 27 | 28 | this.Video = video; 29 | this.SavePath = savePath; 30 | this.BytesToDownload = bytesToDownload; 31 | } 32 | 33 | /// 34 | /// Occurs when the download finished. 35 | /// 36 | public event EventHandler DownloadFinished; 37 | 38 | /// 39 | /// Occurs when the download is starts. 40 | /// 41 | public event EventHandler DownloadStarted; 42 | 43 | /// 44 | /// Gets the number of bytes to download. null, if everything is downloaded. 45 | /// 46 | public int? BytesToDownload { get; private set; } 47 | 48 | /// 49 | /// Gets the path to save the video/audio. 50 | /// 51 | public string SavePath { get; private set; } 52 | 53 | /// 54 | /// Gets the video to download/convert. 55 | /// 56 | public VideoInfo Video { get; private set; } 57 | 58 | /// 59 | /// Starts the work of the . 60 | /// 61 | public abstract Task ExecuteAsync(); 62 | 63 | protected void OnDownloadFinished(EventArgs e) 64 | { 65 | if (this.DownloadFinished != null) 66 | { 67 | this.DownloadFinished(this, e); 68 | } 69 | } 70 | 71 | protected void OnDownloadStarted(EventArgs e) 72 | { 73 | if (this.DownloadStarted != null) 74 | { 75 | this.DownloadStarted(this, e); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/BigEndianBitConverter.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 2006-2012 J.D. Purcell (moitah@yahoo.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | namespace YoutubeExtractorCore 23 | { 24 | internal static class BigEndianBitConverter 25 | { 26 | public static byte[] GetBytes(ulong value) 27 | { 28 | var buff = new byte[8]; 29 | 30 | buff[0] = (byte)(value >> 56); 31 | buff[1] = (byte)(value >> 48); 32 | buff[2] = (byte)(value >> 40); 33 | buff[3] = (byte)(value >> 32); 34 | buff[4] = (byte)(value >> 24); 35 | buff[5] = (byte)(value >> 16); 36 | buff[6] = (byte)(value >> 8); 37 | buff[7] = (byte)(value); 38 | 39 | return buff; 40 | } 41 | 42 | public static byte[] GetBytes(uint value) 43 | { 44 | var buff = new byte[4]; 45 | 46 | buff[0] = (byte)(value >> 24); 47 | buff[1] = (byte)(value >> 16); 48 | buff[2] = (byte)(value >> 8); 49 | buff[3] = (byte)(value); 50 | 51 | return buff; 52 | } 53 | 54 | public static byte[] GetBytes(ushort value) 55 | { 56 | var buff = new byte[2]; 57 | 58 | buff[0] = (byte)(value >> 8); 59 | buff[1] = (byte)(value); 60 | 61 | return buff; 62 | } 63 | 64 | public static ushort ToUInt16(byte[] value, int startIndex) 65 | { 66 | return (ushort)(value[startIndex] << 8 | value[startIndex + 1]); 67 | } 68 | 69 | public static uint ToUInt32(byte[] value, int startIndex) 70 | { 71 | return 72 | (uint)value[startIndex] << 24 | 73 | (uint)value[startIndex + 1] << 16 | 74 | (uint)value[startIndex + 2] << 8 | 75 | value[startIndex + 3]; 76 | } 77 | 78 | public static ulong ToUInt64(byte[] value, int startIndex) 79 | { 80 | return 81 | (ulong)value[startIndex] << 56 | 82 | (ulong)value[startIndex + 1] << 48 | 83 | (ulong)value[startIndex + 2] << 40 | 84 | (ulong)value[startIndex + 3] << 32 | 85 | (ulong)value[startIndex + 4] << 24 | 86 | (ulong)value[startIndex + 5] << 16 | 87 | (ulong)value[startIndex + 6] << 8 | 88 | value[startIndex + 7]; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/VideoDownloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.IO; 6 | using System; 7 | using System.Net; 8 | 9 | namespace YoutubeExtractorCore 10 | { 11 | /// 12 | /// Provides a method to download a video from YouTube. 13 | /// 14 | public class VideoDownloader : Downloader 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The video to download. 20 | /// The path to save the video. 21 | /// An optional value to limit the number of bytes to download. 22 | /// or is null. 23 | public VideoDownloader(VideoInfo video, string savePath, int? bytesToDownload = null) 24 | : base(video, savePath, bytesToDownload) 25 | { } 26 | 27 | /// 28 | /// Occurs when the downlaod progress of the video file has changed. 29 | /// 30 | public event EventHandler DownloadProgressChanged; 31 | 32 | /// 33 | /// Starts the video download. 34 | /// 35 | /// The video file could not be saved. 36 | /// An error occured while downloading the video. 37 | public override async Task ExecuteAsync() 38 | { 39 | this.OnDownloadStarted(EventArgs.Empty); 40 | 41 | var request = (HttpWebRequest)WebRequest.Create(this.Video.DownloadUrl); 42 | 43 | //if (this.BytesToDownload.HasValue) 44 | //{ 45 | // request.AddRange(0, this.BytesToDownload.Value - 1); 46 | //} 47 | 48 | // the following code is alternative, you may implement the function after your needs 49 | using (WebResponse response = await request.GetResponseAsync()) 50 | { 51 | using (Stream source = response.GetResponseStream()) 52 | { 53 | using (FileStream target = File.Open(this.SavePath, FileMode.Create, FileAccess.Write)) 54 | { 55 | var buffer = new byte[1024]; 56 | bool cancel = false; 57 | int bytes; 58 | int copiedBytes = 0; 59 | 60 | while (!cancel && (bytes = source.Read(buffer, 0, buffer.Length)) > 0) 61 | { 62 | target.Write(buffer, 0, bytes); 63 | 64 | copiedBytes += bytes; 65 | 66 | var eventArgs = new ProgressEventArgs((copiedBytes * 1.0 / response.ContentLength) * 100); 67 | 68 | if (this.DownloadProgressChanged != null) 69 | { 70 | this.DownloadProgressChanged(this, eventArgs); 71 | 72 | if (eventArgs.Cancel) 73 | { 74 | cancel = true; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | this.OnDownloadFinished(EventArgs.Empty); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/BitHelper.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 2006-2012 J.D. Purcell (moitah@yahoo.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | using System; 23 | 24 | namespace YoutubeExtractorCore 25 | { 26 | internal static class BitHelper 27 | { 28 | public static byte[] CopyBlock(byte[] bytes, int offset, int length) 29 | { 30 | int startByte = offset / 8; 31 | int endByte = (offset + length - 1) / 8; 32 | int shiftA = offset % 8; 33 | int shiftB = 8 - shiftA; 34 | var dst = new byte[(length + 7) / 8]; 35 | 36 | if (shiftA == 0) 37 | { 38 | Buffer.BlockCopy(bytes, startByte, dst, 0, dst.Length); 39 | } 40 | 41 | else 42 | { 43 | int i; 44 | 45 | for (i = 0; i < endByte - startByte; i++) 46 | { 47 | dst[i] = (byte)(bytes[startByte + i] << shiftA | bytes[startByte + i + 1] >> shiftB); 48 | } 49 | 50 | if (i < dst.Length) 51 | { 52 | dst[i] = (byte)(bytes[startByte + i] << shiftA); 53 | } 54 | } 55 | 56 | dst[dst.Length - 1] &= (byte)(0xFF << dst.Length * 8 - length); 57 | 58 | return dst; 59 | } 60 | 61 | public static void CopyBytes(byte[] dst, int dstOffset, byte[] src) 62 | { 63 | Buffer.BlockCopy(src, 0, dst, dstOffset, src.Length); 64 | } 65 | 66 | public static int Read(ref ulong x, int length) 67 | { 68 | int r = (int)(x >> 64 - length); 69 | x <<= length; 70 | return r; 71 | } 72 | 73 | public static int Read(byte[] bytes, ref int offset, int length) 74 | { 75 | int startByte = offset / 8; 76 | int endByte = (offset + length - 1) / 8; 77 | int skipBits = offset % 8; 78 | ulong bits = 0; 79 | 80 | for (int i = 0; i <= Math.Min(endByte - startByte, 7); i++) 81 | { 82 | bits |= (ulong)bytes[startByte + i] << 56 - i * 8; 83 | } 84 | 85 | if (skipBits != 0) 86 | { 87 | Read(ref bits, skipBits); 88 | } 89 | 90 | offset += length; 91 | 92 | return Read(ref bits, length); 93 | } 94 | 95 | public static void Write(ref ulong x, int length, int value) 96 | { 97 | ulong mask = 0xFFFFFFFFFFFFFFFF >> 64 - length; 98 | x = x << length | (ulong)value & mask; 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/AacAudioExtractor.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 2006-2012 J.D. Purcell (moitah@yahoo.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | using System.IO; 23 | 24 | namespace YoutubeExtractorCore 25 | { 26 | internal class AacAudioExtractor : IAudioExtractor 27 | { 28 | private readonly FileStream fileStream; 29 | private int aacProfile; 30 | private int channelConfig; 31 | private int sampleRateIndex; 32 | 33 | public AacAudioExtractor(string path) 34 | { 35 | this.VideoPath = path; 36 | fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 64 * 1024); 37 | } 38 | 39 | public string VideoPath { get; private set; } 40 | 41 | public void Dispose() 42 | { 43 | this.fileStream.Dispose(); 44 | } 45 | 46 | public void WriteChunk(byte[] chunk, uint timeStamp) 47 | { 48 | if (chunk.Length < 1) 49 | { 50 | return; 51 | } 52 | 53 | if (chunk[0] == 0) 54 | { 55 | // Header 56 | if (chunk.Length < 3) 57 | { 58 | return; 59 | } 60 | 61 | ulong bits = (ulong)BigEndianBitConverter.ToUInt16(chunk, 1) << 48; 62 | 63 | aacProfile = BitHelper.Read(ref bits, 5) - 1; 64 | sampleRateIndex = BitHelper.Read(ref bits, 4); 65 | channelConfig = BitHelper.Read(ref bits, 4); 66 | 67 | if (aacProfile < 0 || aacProfile > 3) 68 | throw new AudioExtractionException("Unsupported AAC profile."); 69 | if (sampleRateIndex > 12) 70 | throw new AudioExtractionException("Invalid AAC sample rate index."); 71 | if (channelConfig > 6) 72 | throw new AudioExtractionException("Invalid AAC channel configuration."); 73 | } 74 | 75 | else 76 | { 77 | // Audio data 78 | int dataSize = chunk.Length - 1; 79 | ulong bits = 0; 80 | 81 | // Reference: WriteADTSHeader from FAAC's bitstream.c 82 | 83 | BitHelper.Write(ref bits, 12, 0xFFF); 84 | BitHelper.Write(ref bits, 1, 0); 85 | BitHelper.Write(ref bits, 2, 0); 86 | BitHelper.Write(ref bits, 1, 1); 87 | BitHelper.Write(ref bits, 2, aacProfile); 88 | BitHelper.Write(ref bits, 4, sampleRateIndex); 89 | BitHelper.Write(ref bits, 1, 0); 90 | BitHelper.Write(ref bits, 3, channelConfig); 91 | BitHelper.Write(ref bits, 1, 0); 92 | BitHelper.Write(ref bits, 1, 0); 93 | BitHelper.Write(ref bits, 1, 0); 94 | BitHelper.Write(ref bits, 1, 0); 95 | BitHelper.Write(ref bits, 13, 7 + dataSize); 96 | BitHelper.Write(ref bits, 11, 0x7FF); 97 | BitHelper.Write(ref bits, 2, 0); 98 | 99 | fileStream.Write(BigEndianBitConverter.GetBytes(bits), 1, 7); 100 | fileStream.Write(chunk, 1, dataSize); 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/AudioDownloader.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 2013-2014 Dennis Daume (daume.dennis@gmail.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | using System; 23 | using System.IO; 24 | using System.Net; 25 | using System.Threading.Tasks; 26 | 27 | namespace YoutubeExtractorCore 28 | { 29 | /// 30 | /// Provides a method to download a video and extract its audio track. 31 | /// 32 | public class AudioDownloader : Downloader 33 | { 34 | private bool isCanceled; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// The video to convert. 40 | /// The path to save the audio. 41 | /// /// An optional value to limit the number of bytes to download. 42 | /// or is null. 43 | public AudioDownloader(VideoInfo video, string savePath, int? bytesToDownload = null) 44 | : base(video, savePath, bytesToDownload) 45 | { } 46 | 47 | /// 48 | /// Occurs when the progress of the audio extraction has changed. 49 | /// 50 | public event EventHandler AudioExtractionProgressChanged; 51 | 52 | /// 53 | /// Occurs when the download progress of the video file has changed. 54 | /// 55 | public event EventHandler DownloadProgressChanged; 56 | 57 | /// 58 | /// Downloads the video from YouTube and then extracts the audio track out if it. 59 | /// 60 | /// 61 | /// The temporary video file could not be created. 62 | /// - or - 63 | /// The audio file could not be created. 64 | /// 65 | /// An error occured during audio extraction. 66 | /// An error occured while downloading the video. 67 | public override Task ExecuteAsync() 68 | { 69 | Task.Yield(); 70 | string tempPath = Path.GetTempFileName(); 71 | 72 | this.DownloadVideo(tempPath); 73 | 74 | if (!this.isCanceled) 75 | { 76 | this.ExtractAudio(tempPath); 77 | } 78 | 79 | this.OnDownloadFinished(EventArgs.Empty); 80 | return Task.FromResult(0); 81 | } 82 | 83 | private async void DownloadVideo(string path) 84 | { 85 | var videoDownloader = new VideoDownloader(this.Video, path, this.BytesToDownload); 86 | 87 | videoDownloader.DownloadProgressChanged += (sender, args) => 88 | { 89 | if (this.DownloadProgressChanged != null) 90 | { 91 | this.DownloadProgressChanged(this, args); 92 | 93 | this.isCanceled = args.Cancel; 94 | } 95 | }; 96 | 97 | await videoDownloader.ExecuteAsync(); 98 | } 99 | 100 | private void ExtractAudio(string path) 101 | { 102 | using (var flvFile = new FlvFile(path, this.SavePath)) 103 | { 104 | flvFile.ConversionProgressChanged += (sender, args) => 105 | { 106 | if (this.AudioExtractionProgressChanged != null) 107 | { 108 | this.AudioExtractionProgressChanged(this, new ProgressEventArgs(args.ProgressPercentage)); 109 | } 110 | }; 111 | 112 | flvFile.ExtractStreams(); 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Microsoft Azure ApplicationInsights config file 170 | ApplicationInsights.config 171 | 172 | # Windows Store app package directory 173 | AppPackages/ 174 | BundleArtifacts/ 175 | 176 | # Visual Studio cache files 177 | # files ending in .cache can be ignored 178 | *.[Cc]ache 179 | # but keep track of directories ending in .cache 180 | !*.[Cc]ache/ 181 | 182 | # Others 183 | ClientBin/ 184 | [Ss]tyle[Cc]op.* 185 | ~$* 186 | *~ 187 | *.dbmdl 188 | *.dbproj.schemaview 189 | *.pfx 190 | *.publishsettings 191 | node_modules/ 192 | orleans.codegen.cs 193 | 194 | # RIA/Silverlight projects 195 | Generated_Code/ 196 | 197 | # Backup & report files from converting an old project file 198 | # to a newer Visual Studio version. Backup files are not needed, 199 | # because we have git ;-) 200 | _UpgradeReport_Files/ 201 | Backup*/ 202 | UpgradeLog*.XML 203 | UpgradeLog*.htm 204 | 205 | # SQL Server files 206 | *.mdf 207 | *.ldf 208 | 209 | # Business Intelligence projects 210 | *.rdl.data 211 | *.bim.layout 212 | *.bim_*.settings 213 | 214 | # Microsoft Fakes 215 | FakesAssemblies/ 216 | 217 | # GhostDoc plugin setting file 218 | *.GhostDoc.xml 219 | 220 | # Node.js Tools for Visual Studio 221 | .ntvs_analysis.dat 222 | 223 | # Visual Studio 6 build log 224 | *.plg 225 | 226 | # Visual Studio 6 workspace options file 227 | *.opt 228 | 229 | # Visual Studio LightSwitch build output 230 | **/*.HTMLClient/GeneratedArtifacts 231 | **/*.DesktopClient/GeneratedArtifacts 232 | **/*.DesktopClient/ModelManifest.xml 233 | **/*.Server/GeneratedArtifacts 234 | **/*.Server/ModelManifest.xml 235 | _Pvt_Extensions 236 | 237 | # LightSwitch generated files 238 | GeneratedArtifacts/ 239 | ModelManifest.xml 240 | 241 | # Paket dependency manager 242 | .paket/paket.exe 243 | 244 | # FAKE - F# Make 245 | .fake/ -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/Decipherer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace YoutubeExtractorCore 8 | { 9 | internal static class Decipherer 10 | { 11 | public static async Task DecipherWithVersionAsync(string cipher, string cipherVersion) 12 | { 13 | string jsUrl = string.Format("http://s.ytimg.com/yts/jsbin/player-{0}.js", cipherVersion); 14 | string js = await HttpHelper.DownloadStringAsync(jsUrl); 15 | 16 | //Find "C" in this: var A = B.sig||C (B.s) 17 | const string functNamePattern = @"\.sig\s*\|\|([a-zA-Z0-9\$]+)\("; //Regex Formed To Find Word or DollarSign 18 | 19 | string funcName = Regex.Match(js, functNamePattern).Groups[1].Value; 20 | 21 | if (funcName.Contains("$")) 22 | { 23 | funcName = "\\" + funcName; //Due To Dollar Sign Introduction, Need To Escape 24 | } 25 | 26 | string funcPattern = @"(?!h\.)" + funcName + @"=function\(\w+\)\{.*?\}"; //Escape funcName string 27 | string funcBody = Regex.Match(js, funcPattern, RegexOptions.Singleline).Value; //Entire sig function 28 | var lines = funcBody.Split(';'); //Each line in sig function 29 | 30 | string idReverse = "", idSlice = "", idCharSwap = ""; //Hold name for each cipher method 31 | string functionIdentifier; 32 | string operations = ""; 33 | 34 | foreach (string line in lines.Skip(1).Take(lines.Length - 2)) //Matches the funcBody with each cipher method. Only runs till all three are defined. 35 | { 36 | if (!string.IsNullOrEmpty(idReverse) && !string.IsNullOrEmpty(idSlice) && 37 | !string.IsNullOrEmpty(idCharSwap)) 38 | { 39 | break; //Break loop if all three cipher methods are defined 40 | } 41 | 42 | functionIdentifier = GetFunctionFromLine(line); 43 | string reReverse = string.Format(@"{0}:\bfunction\b\(\w+\)", functionIdentifier); //Regex for reverse (one parameter) 44 | string reSlice = string.Format(@"{0}:\bfunction\b\([a],b\).(\breturn\b)?.?\w+\.", functionIdentifier); //Regex for slice (return or not) 45 | string reSwap = string.Format(@"{0}:\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b", functionIdentifier); //Regex for the char swap. 46 | 47 | if (Regex.Match(js, reReverse).Success) 48 | { 49 | idReverse = functionIdentifier; //If def matched the regex for reverse then the current function is a defined as the reverse 50 | } 51 | 52 | if (Regex.Match(js, reSlice).Success) 53 | { 54 | idSlice = functionIdentifier; //If def matched the regex for slice then the current function is defined as the slice. 55 | } 56 | 57 | if (Regex.Match(js, reSwap).Success) 58 | { 59 | idCharSwap = functionIdentifier; //If def matched the regex for charSwap then the current function is defined as swap. 60 | } 61 | } 62 | 63 | foreach (string line in lines.Skip(1).Take(lines.Length - 2)) 64 | { 65 | Match m; 66 | functionIdentifier = GetFunctionFromLine(line); 67 | 68 | if ((m = Regex.Match(line, @"\(\w+,(?\d+)\)")).Success && functionIdentifier == idCharSwap) 69 | { 70 | operations += "w" + m.Groups["index"].Value + " "; //operation is a swap (w) 71 | } 72 | 73 | if ((m = Regex.Match(line, @"\(\w+,(?\d+)\)")).Success && functionIdentifier == idSlice) 74 | { 75 | operations += "s" + m.Groups["index"].Value + " "; //operation is a slice 76 | } 77 | 78 | if (functionIdentifier == idReverse) //No regex required for reverse (reverse method has no parameters) 79 | { 80 | operations += "r "; //operation is a reverse 81 | } 82 | } 83 | 84 | operations = operations.Trim(); 85 | 86 | return DecipherWithOperations(cipher, operations); 87 | } 88 | 89 | private static string ApplyOperation(string cipher, string op) 90 | { 91 | switch (op[0]) 92 | { 93 | case 'r': 94 | return new string(cipher.ToCharArray().Reverse().ToArray()); 95 | 96 | case 'w': 97 | { 98 | int index = GetOpIndex(op); 99 | return SwapFirstChar(cipher, index); 100 | } 101 | 102 | case 's': 103 | { 104 | int index = GetOpIndex(op); 105 | return cipher.Substring(index); 106 | } 107 | 108 | default: 109 | throw new NotImplementedException("Couldn't find cipher operation."); 110 | } 111 | } 112 | 113 | private static string DecipherWithOperations(string cipher, string operations) 114 | { 115 | return operations.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries) 116 | .Aggregate(cipher, ApplyOperation); 117 | } 118 | 119 | private static string GetFunctionFromLine(string currentLine) 120 | { 121 | var matchFunctionReg = new Regex(@"\w+\.(?\w+)\("); //lc.ac(b,c) want the ac part. 122 | var rgMatch = matchFunctionReg.Match(currentLine); 123 | string matchedFunction = rgMatch.Groups["functionID"].Value; 124 | return matchedFunction; //return 'ac' 125 | } 126 | 127 | private static int GetOpIndex(string op) 128 | { 129 | string parsed = new Regex(@".(\d+)").Match(op).Result("$1"); 130 | int index = int.Parse(parsed); 131 | 132 | return index; 133 | } 134 | 135 | private static string SwapFirstChar(string cipher, int index) 136 | { 137 | var builder = new StringBuilder(cipher); 138 | builder[0] = cipher[index]; 139 | builder[index] = cipher[0]; 140 | 141 | return builder.ToString(); 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/FlvFile.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 2006-2012 J.D. Purcell (moitah@yahoo.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | using System; 23 | using System.IO; 24 | 25 | namespace YoutubeExtractorCore 26 | { 27 | internal class FlvFile : IDisposable 28 | { 29 | private readonly long fileLength; 30 | private readonly string inputPath; 31 | private readonly string outputPath; 32 | private IAudioExtractor audioExtractor; 33 | private long fileOffset; 34 | private FileStream fileStream; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// The path of the input. 40 | /// The path of the output without extension. 41 | public FlvFile(string inputPath, string outputPath) 42 | { 43 | this.inputPath = inputPath; 44 | this.outputPath = outputPath; 45 | this.fileStream = new FileStream(this.inputPath, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024); 46 | this.fileOffset = 0; 47 | this.fileLength = fileStream.Length; 48 | } 49 | 50 | public event EventHandler ConversionProgressChanged; 51 | 52 | public bool ExtractedAudio { get; private set; } 53 | 54 | public void Dispose() 55 | { 56 | this.Dispose(true); 57 | GC.SuppressFinalize(this); 58 | } 59 | 60 | /// The input file is not an FLV file. 61 | public void ExtractStreams() 62 | { 63 | this.Seek(0); 64 | 65 | if (this.ReadUInt32() != 0x464C5601) 66 | { 67 | // not a FLV file 68 | throw new AudioExtractionException("Invalid input file. Impossible to extract audio track."); 69 | } 70 | 71 | this.ReadUInt8(); 72 | uint dataOffset = this.ReadUInt32(); 73 | 74 | this.Seek(dataOffset); 75 | 76 | this.ReadUInt32(); 77 | 78 | while (fileOffset < fileLength) 79 | { 80 | if (!ReadTag()) 81 | { 82 | break; 83 | } 84 | 85 | if (fileLength - fileOffset < 4) 86 | { 87 | break; 88 | } 89 | 90 | this.ReadUInt32(); 91 | 92 | double progress = (this.fileOffset * 1.0 / this.fileLength) * 100; 93 | 94 | if (this.ConversionProgressChanged != null) 95 | { 96 | this.ConversionProgressChanged(this, new ProgressEventArgs(progress)); 97 | } 98 | } 99 | 100 | this.CloseOutput(false); 101 | } 102 | 103 | private void CloseOutput(bool disposing) 104 | { 105 | if (this.audioExtractor != null) 106 | { 107 | if (disposing && this.audioExtractor.VideoPath != null) 108 | { 109 | try 110 | { 111 | File.Delete(this.audioExtractor.VideoPath); 112 | } 113 | catch { } 114 | } 115 | 116 | this.audioExtractor.Dispose(); 117 | this.audioExtractor = null; 118 | } 119 | } 120 | 121 | private void Dispose(bool disposing) 122 | { 123 | if (disposing) 124 | { 125 | if (this.fileStream != null) 126 | { 127 | this.fileStream.Dispose(); 128 | this.fileStream = null; 129 | } 130 | 131 | this.CloseOutput(true); 132 | } 133 | } 134 | 135 | private IAudioExtractor GetAudioWriter(uint mediaInfo) 136 | { 137 | uint format = mediaInfo >> 4; 138 | 139 | switch (format) 140 | { 141 | case 14: 142 | case 2: 143 | return new Mp3AudioExtractor(this.outputPath); 144 | 145 | case 10: 146 | return new AacAudioExtractor(this.outputPath); 147 | } 148 | 149 | string typeStr; 150 | 151 | switch (format) 152 | { 153 | case 1: 154 | typeStr = "ADPCM"; 155 | break; 156 | 157 | case 6: 158 | case 5: 159 | case 4: 160 | typeStr = "Nellymoser"; 161 | break; 162 | 163 | default: 164 | typeStr = "format=" + format; 165 | break; 166 | } 167 | 168 | throw new AudioExtractionException("Unable to extract audio (" + typeStr + " is unsupported)."); 169 | } 170 | 171 | private byte[] ReadBytes(int length) 172 | { 173 | var buff = new byte[length]; 174 | 175 | this.fileStream.Read(buff, 0, length); 176 | this.fileOffset += length; 177 | 178 | return buff; 179 | } 180 | 181 | private bool ReadTag() 182 | { 183 | if (this.fileLength - this.fileOffset < 11) 184 | return false; 185 | 186 | // Read tag header 187 | uint tagType = ReadUInt8(); 188 | uint dataSize = ReadUInt24(); 189 | uint timeStamp = ReadUInt24(); 190 | timeStamp |= this.ReadUInt8() << 24; 191 | this.ReadUInt24(); 192 | 193 | // Read tag data 194 | if (dataSize == 0) 195 | return true; 196 | 197 | if (this.fileLength - this.fileOffset < dataSize) 198 | return false; 199 | 200 | uint mediaInfo = this.ReadUInt8(); 201 | dataSize -= 1; 202 | byte[] data = this.ReadBytes((int)dataSize); 203 | 204 | if (tagType == 0x8) 205 | { 206 | // If we have no audio writer, create one 207 | if (this.audioExtractor == null) 208 | { 209 | this.audioExtractor = this.GetAudioWriter(mediaInfo); 210 | this.ExtractedAudio = this.audioExtractor != null; 211 | } 212 | 213 | if (this.audioExtractor == null) 214 | { 215 | throw new InvalidOperationException("No supported audio writer found."); 216 | } 217 | 218 | this.audioExtractor.WriteChunk(data, timeStamp); 219 | } 220 | 221 | return true; 222 | } 223 | 224 | private uint ReadUInt24() 225 | { 226 | var x = new byte[4]; 227 | 228 | this.fileStream.Read(x, 1, 3); 229 | this.fileOffset += 3; 230 | 231 | return BigEndianBitConverter.ToUInt32(x, 0); 232 | } 233 | 234 | private uint ReadUInt32() 235 | { 236 | var x = new byte[4]; 237 | 238 | this.fileStream.Read(x, 0, 4); 239 | this.fileOffset += 4; 240 | 241 | return BigEndianBitConverter.ToUInt32(x, 0); 242 | } 243 | 244 | private uint ReadUInt8() 245 | { 246 | this.fileOffset += 1; 247 | return (uint)this.fileStream.ReadByte(); 248 | } 249 | 250 | private void Seek(long offset) 251 | { 252 | this.fileStream.Seek(offset, SeekOrigin.Begin); 253 | this.fileOffset = offset; 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/VideoInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace YoutubeExtractorCore 4 | { 5 | public class VideoInfo 6 | { 7 | internal static readonly IEnumerable Defaults = new List 8 | { 9 | /* Non-adaptive */ 10 | new VideoInfo(5, VideoType.Flash, 240, false, AudioType.Mp3, 64, AdaptiveType.None), 11 | new VideoInfo(6, VideoType.Flash, 270, false, AudioType.Mp3, 64, AdaptiveType.None), 12 | new VideoInfo(13, VideoType.Mobile, 0, false, AudioType.Aac, 0, AdaptiveType.None), 13 | new VideoInfo(17, VideoType.Mobile, 144, false, AudioType.Aac, 24, AdaptiveType.None), 14 | new VideoInfo(18, VideoType.Mp4, 360, false, AudioType.Aac, 96, AdaptiveType.None), 15 | new VideoInfo(22, VideoType.Mp4, 720, false, AudioType.Aac, 192, AdaptiveType.None), 16 | new VideoInfo(34, VideoType.Flash, 360, false, AudioType.Aac, 128, AdaptiveType.None), 17 | new VideoInfo(35, VideoType.Flash, 480, false, AudioType.Aac, 128, AdaptiveType.None), 18 | new VideoInfo(36, VideoType.Mobile, 240, false, AudioType.Aac, 38, AdaptiveType.None), 19 | new VideoInfo(37, VideoType.Mp4, 1080, false, AudioType.Aac, 192, AdaptiveType.None), 20 | new VideoInfo(38, VideoType.Mp4, 3072, false, AudioType.Aac, 192, AdaptiveType.None), 21 | new VideoInfo(43, VideoType.WebM, 360, false, AudioType.Vorbis, 128, AdaptiveType.None), 22 | new VideoInfo(44, VideoType.WebM, 480, false, AudioType.Vorbis, 128, AdaptiveType.None), 23 | new VideoInfo(45, VideoType.WebM, 720, false, AudioType.Vorbis, 192, AdaptiveType.None), 24 | new VideoInfo(46, VideoType.WebM, 1080, false, AudioType.Vorbis, 192, AdaptiveType.None), 25 | 26 | /* 3d */ 27 | new VideoInfo(82, VideoType.Mp4, 360, true, AudioType.Aac, 96, AdaptiveType.None), 28 | new VideoInfo(83, VideoType.Mp4, 240, true, AudioType.Aac, 96, AdaptiveType.None), 29 | new VideoInfo(84, VideoType.Mp4, 720, true, AudioType.Aac, 152, AdaptiveType.None), 30 | new VideoInfo(85, VideoType.Mp4, 520, true, AudioType.Aac, 152, AdaptiveType.None), 31 | new VideoInfo(100, VideoType.WebM, 360, true, AudioType.Vorbis, 128, AdaptiveType.None), 32 | new VideoInfo(101, VideoType.WebM, 360, true, AudioType.Vorbis, 192, AdaptiveType.None), 33 | new VideoInfo(102, VideoType.WebM, 720, true, AudioType.Vorbis, 192, AdaptiveType.None), 34 | 35 | /* Adaptive (aka DASH) - Video */ 36 | new VideoInfo(133, VideoType.Mp4, 240, false, AudioType.Unknown, 0, AdaptiveType.Video), 37 | new VideoInfo(134, VideoType.Mp4, 360, false, AudioType.Unknown, 0, AdaptiveType.Video), 38 | new VideoInfo(135, VideoType.Mp4, 480, false, AudioType.Unknown, 0, AdaptiveType.Video), 39 | new VideoInfo(136, VideoType.Mp4, 720, false, AudioType.Unknown, 0, AdaptiveType.Video), 40 | new VideoInfo(137, VideoType.Mp4, 1080, false, AudioType.Unknown, 0, AdaptiveType.Video), 41 | new VideoInfo(138, VideoType.Mp4, 2160, false, AudioType.Unknown, 0, AdaptiveType.Video), 42 | new VideoInfo(160, VideoType.Mp4, 144, false, AudioType.Unknown, 0, AdaptiveType.Video), 43 | new VideoInfo(242, VideoType.WebM, 240, false, AudioType.Unknown, 0, AdaptiveType.Video), 44 | new VideoInfo(243, VideoType.WebM, 360, false, AudioType.Unknown, 0, AdaptiveType.Video), 45 | new VideoInfo(244, VideoType.WebM, 480, false, AudioType.Unknown, 0, AdaptiveType.Video), 46 | new VideoInfo(247, VideoType.WebM, 720, false, AudioType.Unknown, 0, AdaptiveType.Video), 47 | new VideoInfo(248, VideoType.WebM, 1080, false, AudioType.Unknown, 0, AdaptiveType.Video), 48 | new VideoInfo(264, VideoType.Mp4, 1440, false, AudioType.Unknown, 0, AdaptiveType.Video), 49 | new VideoInfo(271, VideoType.WebM, 1440, false, AudioType.Unknown, 0, AdaptiveType.Video), 50 | new VideoInfo(272, VideoType.WebM, 2160, false, AudioType.Unknown, 0, AdaptiveType.Video), 51 | new VideoInfo(278, VideoType.WebM, 144, false, AudioType.Unknown, 0, AdaptiveType.Video), 52 | 53 | /* Adaptive (aka DASH) - Audio */ 54 | new VideoInfo(139, VideoType.Mp4, 0, false, AudioType.Aac, 48, AdaptiveType.Audio), 55 | new VideoInfo(140, VideoType.Mp4, 0, false, AudioType.Aac, 128, AdaptiveType.Audio), 56 | new VideoInfo(141, VideoType.Mp4, 0, false, AudioType.Aac, 256, AdaptiveType.Audio), 57 | new VideoInfo(171, VideoType.WebM, 0, false, AudioType.Vorbis, 128, AdaptiveType.Audio), 58 | new VideoInfo(172, VideoType.WebM, 0, false, AudioType.Vorbis, 192, AdaptiveType.Audio) 59 | }; 60 | 61 | internal VideoInfo(int formatCode) 62 | : this(formatCode, VideoType.Unknown, 0, false, AudioType.Unknown, 0, AdaptiveType.None) 63 | { } 64 | 65 | internal VideoInfo(VideoInfo info) 66 | : this(info.FormatCode, info.VideoType, info.Resolution, info.Is3D, info.AudioType, info.AudioBitrate, info.AdaptiveType) 67 | { } 68 | 69 | private VideoInfo(int formatCode, VideoType videoType, int resolution, bool is3D, AudioType audioType, int audioBitrate, AdaptiveType adaptiveType) 70 | { 71 | FormatCode = formatCode; 72 | VideoType = videoType; 73 | Resolution = resolution; 74 | Is3D = is3D; 75 | AudioType = audioType; 76 | AudioBitrate = audioBitrate; 77 | AdaptiveType = adaptiveType; 78 | } 79 | 80 | /// 81 | /// Gets an enum indicating whether the format is adaptive or not. 82 | /// 83 | /// 84 | /// AdaptiveType.Audio or AdaptiveType.Video if the format is adaptive; 85 | /// otherwise, AdaptiveType.None. 86 | /// 87 | public AdaptiveType AdaptiveType { get; } 88 | 89 | /// 90 | /// The approximate audio bitrate in kbit/s. 91 | /// 92 | /// The approximate audio bitrate in kbit/s, or 0 if the bitrate is unknown. 93 | public int AudioBitrate { get; } 94 | 95 | /// 96 | /// Gets the audio type (encoding). 97 | /// 98 | public AudioType AudioType { get; } 99 | 100 | /// 101 | /// Gets the download URL. 102 | /// 103 | public string DownloadUrl { get; internal set; } 104 | 105 | /// 106 | /// Gets the format code, that is used by YouTube internally to differentiate between 107 | /// quality profiles. 108 | /// 109 | public int FormatCode { get; } 110 | 111 | public bool Is3D { get; } 112 | 113 | /// 114 | /// Gets a value indicating whether this video info requires a signature decryption before 115 | /// the download URL can be used. 116 | /// 117 | /// This can be achieved with the 118 | /// 119 | public bool RequiresDecryption { get; internal set; } 120 | 121 | /// 122 | /// Gets the resolution of the video. 123 | /// 124 | /// The resolution of the video, or 0 if the resolution is unkown. 125 | public int Resolution { get; } 126 | 127 | /// 128 | /// Gets the video title. 129 | /// 130 | public string Title { get; internal set; } 131 | 132 | /// 133 | /// Gets the video extension. 134 | /// 135 | /// The video extension, or null if the video extension is unknown. 136 | public string VideoExtension 137 | { 138 | get 139 | { 140 | switch (VideoType) 141 | { 142 | case VideoType.Mp4: 143 | return ".mp4"; 144 | 145 | case VideoType.Mobile: 146 | return ".3gp"; 147 | 148 | case VideoType.Flash: 149 | return ".flv"; 150 | 151 | case VideoType.WebM: 152 | return ".webm"; 153 | } 154 | 155 | return null; 156 | } 157 | } 158 | 159 | /// 160 | /// Gets the video type (container). 161 | /// 162 | public VideoType VideoType { get; } 163 | 164 | /// 165 | /// We use this in the method to 166 | /// decrypt the signature 167 | /// 168 | /// 169 | internal string HtmlPlayerVersion { get; set; } 170 | 171 | public override string ToString() 172 | { 173 | return string.Format("Full Title: {0}, Type: {1}, Resolution: {2}p", Title + VideoExtension, VideoType, Resolution); 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/Mp3AudioExtractor.cs: -------------------------------------------------------------------------------- 1 | // **************************************************************************** 2 | // 3 | // FLV Extract 4 | // Copyright (C) 2006-2012 J.D. Purcell (moitah@yahoo.com) 5 | // 6 | // This program is free software; you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation; either version 2 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | // along with this program; if not, write to the Free Software 18 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | // 20 | // **************************************************************************** 21 | 22 | using System.Collections.Generic; 23 | using System.IO; 24 | 25 | namespace YoutubeExtractorCore 26 | { 27 | internal class Mp3AudioExtractor : IAudioExtractor 28 | { 29 | private readonly List chunkBuffer; 30 | private readonly FileStream fileStream; 31 | private readonly List frameOffsets; 32 | private readonly List warnings; 33 | private int channelMode; 34 | private bool delayWrite; 35 | private int firstBitRate; 36 | private uint firstFrameHeader; 37 | private bool hasVbrHeader; 38 | private bool isVbr; 39 | private int mpegVersion; 40 | private int sampleRate; 41 | private uint totalFrameLength; 42 | private bool writeVbrHeader; 43 | 44 | public Mp3AudioExtractor(string path) 45 | { 46 | this.VideoPath = path; 47 | this.fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 64 * 1024); 48 | this.warnings = new List(); 49 | this.chunkBuffer = new List(); 50 | this.frameOffsets = new List(); 51 | this.delayWrite = true; 52 | } 53 | 54 | public string VideoPath { get; private set; } 55 | 56 | public IEnumerable Warnings 57 | { 58 | get { return this.warnings; } 59 | } 60 | 61 | public void Dispose() 62 | { 63 | this.Flush(); 64 | 65 | if (this.writeVbrHeader) 66 | { 67 | this.fileStream.Seek(0, SeekOrigin.Begin); 68 | this.WriteVbrHeader(false); 69 | } 70 | 71 | this.fileStream.Dispose(); 72 | } 73 | 74 | public void WriteChunk(byte[] chunk, uint timeStamp) 75 | { 76 | this.chunkBuffer.Add(chunk); 77 | this.ParseMp3Frames(chunk); 78 | 79 | if (this.delayWrite && this.totalFrameLength >= 65536) 80 | { 81 | this.delayWrite = false; 82 | } 83 | 84 | if (!this.delayWrite) 85 | { 86 | this.Flush(); 87 | } 88 | } 89 | 90 | private static int GetFrameDataOffset(int mpegVersion, int channelMode) 91 | { 92 | return 4 + (mpegVersion == 3 ? 93 | (channelMode == 3 ? 17 : 32) : 94 | (channelMode == 3 ? 9 : 17)); 95 | } 96 | 97 | private static int GetFrameLength(int mpegVersion, int bitRate, int sampleRate, int padding) 98 | { 99 | return (mpegVersion == 3 ? 144 : 72) * bitRate / sampleRate + padding; 100 | } 101 | 102 | private void Flush() 103 | { 104 | foreach (byte[] chunk in chunkBuffer) 105 | { 106 | this.fileStream.Write(chunk, 0, chunk.Length); 107 | } 108 | 109 | this.chunkBuffer.Clear(); 110 | } 111 | 112 | private void ParseMp3Frames(byte[] buffer) 113 | { 114 | var mpeg1BitRate = new[] { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 }; 115 | var mpeg2XBitRate = new[] { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }; 116 | var mpeg1SampleRate = new[] { 44100, 48000, 32000, 0 }; 117 | var mpeg20SampleRate = new[] { 22050, 24000, 16000, 0 }; 118 | var mpeg25SampleRate = new[] { 11025, 12000, 8000, 0 }; 119 | 120 | int offset = 0; 121 | int length = buffer.Length; 122 | 123 | while (length >= 4) 124 | { 125 | int mpegVersion, sampleRate, channelMode; 126 | 127 | ulong header = (ulong)BigEndianBitConverter.ToUInt32(buffer, offset) << 32; 128 | 129 | if (BitHelper.Read(ref header, 11) != 0x7FF) 130 | { 131 | break; 132 | } 133 | 134 | mpegVersion = BitHelper.Read(ref header, 2); 135 | int layer = BitHelper.Read(ref header, 2); 136 | BitHelper.Read(ref header, 1); 137 | int bitRate = BitHelper.Read(ref header, 4); 138 | sampleRate = BitHelper.Read(ref header, 2); 139 | int padding = BitHelper.Read(ref header, 1); 140 | BitHelper.Read(ref header, 1); 141 | channelMode = BitHelper.Read(ref header, 2); 142 | 143 | if (mpegVersion == 1 || layer != 1 || bitRate == 0 || bitRate == 15 || sampleRate == 3) 144 | { 145 | break; 146 | } 147 | 148 | bitRate = (mpegVersion == 3 ? mpeg1BitRate[bitRate] : mpeg2XBitRate[bitRate]) * 1000; 149 | 150 | switch (mpegVersion) 151 | { 152 | case 2: 153 | sampleRate = mpeg20SampleRate[sampleRate]; 154 | break; 155 | 156 | case 3: 157 | sampleRate = mpeg1SampleRate[sampleRate]; 158 | break; 159 | 160 | default: 161 | sampleRate = mpeg25SampleRate[sampleRate]; 162 | break; 163 | } 164 | 165 | int frameLenght = GetFrameLength(mpegVersion, bitRate, sampleRate, padding); 166 | 167 | if (frameLenght > length) 168 | { 169 | break; 170 | } 171 | 172 | bool isVbrHeaderFrame = false; 173 | 174 | if (frameOffsets.Count == 0) 175 | { 176 | // Check for an existing VBR header just to be safe (I haven't seen any in FLVs) 177 | int o = offset + GetFrameDataOffset(mpegVersion, channelMode); 178 | 179 | if (BigEndianBitConverter.ToUInt32(buffer, o) == 0x58696E67) 180 | { 181 | // "Xing" 182 | isVbrHeaderFrame = true; 183 | this.delayWrite = false; 184 | this.hasVbrHeader = true; 185 | } 186 | } 187 | 188 | if (!isVbrHeaderFrame) 189 | { 190 | if (this.firstBitRate == 0) 191 | { 192 | this.firstBitRate = bitRate; 193 | this.mpegVersion = mpegVersion; 194 | this.sampleRate = sampleRate; 195 | this.channelMode = channelMode; 196 | this.firstFrameHeader = BigEndianBitConverter.ToUInt32(buffer, offset); 197 | } 198 | 199 | else if (!this.isVbr && bitRate != this.firstBitRate) 200 | { 201 | this.isVbr = true; 202 | 203 | if (!this.hasVbrHeader) 204 | { 205 | if (this.delayWrite) 206 | { 207 | this.WriteVbrHeader(true); 208 | this.writeVbrHeader = true; 209 | this.delayWrite = false; 210 | } 211 | 212 | else 213 | { 214 | this.warnings.Add("Detected VBR too late, cannot add VBR header."); 215 | } 216 | } 217 | } 218 | } 219 | 220 | this.frameOffsets.Add(this.totalFrameLength + (uint)offset); 221 | 222 | offset += frameLenght; 223 | length -= frameLenght; 224 | } 225 | 226 | this.totalFrameLength += (uint)buffer.Length; 227 | } 228 | 229 | private void WriteVbrHeader(bool isPlaceholder) 230 | { 231 | var buffer = new byte[GetFrameLength(this.mpegVersion, 64000, this.sampleRate, 0)]; 232 | 233 | if (!isPlaceholder) 234 | { 235 | uint header = this.firstFrameHeader; 236 | int dataOffset = GetFrameDataOffset(this.mpegVersion, this.channelMode); 237 | header &= 0xFFFE0DFF; // Clear CRC, bitrate, and padding fields 238 | header |= (uint)(mpegVersion == 3 ? 5 : 8) << 12; // 64 kbit/sec 239 | BitHelper.CopyBytes(buffer, 0, BigEndianBitConverter.GetBytes(header)); 240 | BitHelper.CopyBytes(buffer, dataOffset, BigEndianBitConverter.GetBytes(0x58696E67)); // "Xing" 241 | BitHelper.CopyBytes(buffer, dataOffset + 4, BigEndianBitConverter.GetBytes((uint)0x7)); // Flags 242 | BitHelper.CopyBytes(buffer, dataOffset + 8, BigEndianBitConverter.GetBytes((uint)frameOffsets.Count)); // Frame count 243 | BitHelper.CopyBytes(buffer, dataOffset + 12, BigEndianBitConverter.GetBytes(totalFrameLength)); // File length 244 | 245 | for (int i = 0; i < 100; i++) 246 | { 247 | int frameIndex = (int)((i / 100.0) * this.frameOffsets.Count); 248 | 249 | buffer[dataOffset + 16 + i] = (byte)(this.frameOffsets[frameIndex] / (double)this.totalFrameLength * 256.0); 250 | } 251 | } 252 | 253 | this.fileStream.Write(buffer, 0, buffer.Length); 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore/DownloadUrlResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace YoutubeExtractorCore 9 | { 10 | /// 11 | /// Provides a method to get the download link of a YouTube video. 12 | /// 13 | public static class DownloadUrlResolver 14 | { 15 | private const string RateBypassFlag = "ratebypass"; 16 | private const string SignatureQuery = "signature"; 17 | 18 | /// 19 | /// Decrypts the signature in the property and sets it 20 | /// to the decrypted URL. Use this method, if you have decryptSignature in the method set to false. 22 | /// 23 | /// The video info which's downlaod URL should be decrypted. 24 | /// 25 | /// There was an error while deciphering the signature. 26 | /// 27 | public static async Task DecryptDownloadUrl(VideoInfo videoInfo) 28 | { 29 | var queries = HttpHelper.ParseQueryString(videoInfo.DownloadUrl); 30 | 31 | if (!queries.ContainsKey(SignatureQuery)) return; 32 | string encryptedSignature = queries[SignatureQuery]; 33 | 34 | string decrypted; 35 | 36 | try 37 | { 38 | decrypted = await GetDecipheredSignatureAsync(videoInfo.HtmlPlayerVersion, encryptedSignature); 39 | } 40 | 41 | catch (Exception ex) 42 | { 43 | throw new YoutubeParseException("Could not decipher signature", ex); 44 | } 45 | 46 | videoInfo.DownloadUrl = HttpHelper.ReplaceQueryStringParameter(videoInfo.DownloadUrl, SignatureQuery, 47 | decrypted); 48 | videoInfo.RequiresDecryption = false; 49 | } 50 | 51 | /// 52 | /// Gets a list of s for the specified URL. 53 | /// 54 | /// The URL of the YouTube video. 55 | /// 56 | /// A value indicating whether the video signatures should be decrypted or not. Decrypting 57 | /// consists of a HTTP request for each , so you may want to set 58 | /// this to false and call on your selected later. 60 | /// 61 | /// A list of s that can be used to download the video. 62 | /// 63 | /// The parameter is null. 64 | /// 65 | /// 66 | /// The parameter is not a valid YouTube URL. 67 | /// 68 | /// The video is not available. 69 | /// The Youtube page could not be parsed. 70 | public static async Task> GetDownloadUrlsAsync(string videoUrl, bool decryptSignature = true) 71 | { 72 | if (videoUrl == null) 73 | throw new ArgumentNullException(nameof(videoUrl)); 74 | 75 | bool isYoutubeUrl = TryNormalizeYoutubeUrl(videoUrl, out videoUrl); 76 | 77 | if (!isYoutubeUrl) 78 | { 79 | throw new ArgumentException("URL is not a valid youtube URL!"); 80 | } 81 | 82 | string pageSource = await HttpHelper.DownloadStringAsync(videoUrl); 83 | if (IsVideoUnavailable(pageSource)) 84 | { 85 | throw new YoutubeVideoNotAvailableException(); 86 | } 87 | 88 | try 89 | { 90 | var json = LoadJson(pageSource); 91 | 92 | string videoTitle = GetVideoTitle(json); 93 | 94 | var downloadUrls = ExtractDownloadUrls(json); 95 | 96 | var infos = GetVideoInfos(downloadUrls, videoTitle).ToList(); 97 | 98 | string htmlPlayerVersion = GetHtml5PlayerVersion(json); 99 | 100 | foreach (var info in infos) 101 | { 102 | info.HtmlPlayerVersion = htmlPlayerVersion; 103 | 104 | if (decryptSignature && info.RequiresDecryption) 105 | { 106 | await DecryptDownloadUrl(info); 107 | } 108 | } 109 | 110 | return infos; 111 | } 112 | 113 | catch (Exception ex) 114 | { 115 | ThrowYoutubeParseException(ex, videoUrl); 116 | } 117 | 118 | return null; // Will never happen, but the compiler requires it 119 | } 120 | 121 | /// 122 | /// Normalizes the given YouTube URL to the format http://youtube.com/watch?v={youtube-id} 123 | /// and returns whether the normalization was successful or not. 124 | /// 125 | /// The YouTube URL to normalize. 126 | /// The normalized YouTube URL. 127 | /// 128 | /// true, if the normalization was successful; false, if the URL is invalid. 129 | /// 130 | public static bool TryNormalizeYoutubeUrl(string url, out string normalizedUrl) 131 | { 132 | url = url.Trim(); 133 | 134 | url = url.Replace("youtu.be/", "youtube.com/watch?v="); 135 | url = url.Replace("www.youtube", "youtube"); 136 | url = url.Replace("youtube.com/embed/", "youtube.com/watch?v="); 137 | 138 | if (url.Contains("/v/")) 139 | { 140 | url = "http://youtube.com" + new Uri(url).AbsolutePath.Replace("/v/", "/watch?v="); 141 | } 142 | 143 | url = url.Replace("/watch#", "/watch?"); 144 | 145 | var query = HttpHelper.ParseQueryString(url); 146 | 147 | string v; 148 | 149 | if (!query.TryGetValue("v", out v)) 150 | { 151 | normalizedUrl = null; 152 | return false; 153 | } 154 | 155 | normalizedUrl = "http://youtube.com/watch?v=" + v; 156 | 157 | return true; 158 | } 159 | 160 | private static IEnumerable ExtractDownloadUrls(JObject json) 161 | { 162 | var splitByUrls = GetStreamMap(json).Split(','); 163 | var adaptiveFmtSplitByUrls = GetAdaptiveStreamMap(json).Split(','); 164 | splitByUrls = splitByUrls.Concat(adaptiveFmtSplitByUrls).ToArray(); 165 | 166 | foreach (string s in splitByUrls) 167 | { 168 | var queries = HttpHelper.ParseQueryString(s); 169 | string url; 170 | 171 | bool requiresDecryption = false; 172 | 173 | if (queries.ContainsKey("s") || queries.ContainsKey("sig")) 174 | { 175 | requiresDecryption = queries.ContainsKey("s"); 176 | string signature = queries.ContainsKey("s") ? queries["s"] : queries["sig"]; 177 | 178 | url = string.Format("{0}&{1}={2}", queries["url"], SignatureQuery, signature); 179 | 180 | string fallbackHost = queries.ContainsKey("fallback_host") 181 | ? "&fallback_host=" + queries["fallback_host"] 182 | : string.Empty; 183 | 184 | url += fallbackHost; 185 | } 186 | 187 | else 188 | { 189 | url = queries["url"]; 190 | } 191 | 192 | url = HttpHelper.UrlDecode(url); 193 | url = HttpHelper.UrlDecode(url); 194 | 195 | var parameters = HttpHelper.ParseQueryString(url); 196 | if (!parameters.ContainsKey(RateBypassFlag)) 197 | url += string.Format("&{0}={1}", RateBypassFlag, "yes"); 198 | 199 | yield return new ExtractionInfo {RequiresDecryption = requiresDecryption, Uri = new Uri(url)}; 200 | } 201 | } 202 | 203 | private static string GetAdaptiveStreamMap(JObject json) 204 | { 205 | // bugfix: adaptive_fmts is missing in some videos, use url_encoded_fmt_stream_map instead 206 | var streamMap = json["args"]["adaptive_fmts"] ?? json["args"]["url_encoded_fmt_stream_map"]; 207 | 208 | return streamMap.ToString(); 209 | } 210 | 211 | private static async Task GetDecipheredSignatureAsync(string htmlPlayerVersion, string signature) 212 | { 213 | return await Decipherer.DecipherWithVersionAsync(signature, htmlPlayerVersion); 214 | } 215 | 216 | private static string GetHtml5PlayerVersion(JObject json) 217 | { 218 | var regex = new Regex(@"player-(.+?).js"); 219 | 220 | string js = json["assets"]["js"].ToString(); 221 | 222 | return regex.Match(js).Result("$1"); 223 | } 224 | 225 | private static string GetStreamMap(JObject json) 226 | { 227 | var streamMap = json["args"]["url_encoded_fmt_stream_map"]; 228 | 229 | string streamMapString = streamMap?.ToString(); 230 | 231 | if (streamMapString == null || streamMapString.Contains("been+removed")) 232 | { 233 | throw new YoutubeVideoNotAvailableException("Video is removed or has an age restriction."); 234 | } 235 | 236 | return streamMapString; 237 | } 238 | 239 | private static IEnumerable GetVideoInfos(IEnumerable extractionInfos, 240 | string videoTitle) 241 | { 242 | var downLoadInfos = new List(); 243 | 244 | foreach (var extractionInfo in extractionInfos) 245 | { 246 | string itag = HttpHelper.ParseQueryString(extractionInfo.Uri.Query)["itag"]; 247 | 248 | int formatCode = int.Parse(itag); 249 | 250 | var info = VideoInfo.Defaults.SingleOrDefault(videoInfo => videoInfo.FormatCode == formatCode); 251 | 252 | if (info != null) 253 | { 254 | info = new VideoInfo(info) 255 | { 256 | DownloadUrl = extractionInfo.Uri.ToString(), 257 | Title = videoTitle, 258 | RequiresDecryption = extractionInfo.RequiresDecryption 259 | }; 260 | } 261 | 262 | else 263 | { 264 | info = new VideoInfo(formatCode) 265 | { 266 | DownloadUrl = extractionInfo.Uri.ToString() 267 | }; 268 | } 269 | 270 | downLoadInfos.Add(info); 271 | } 272 | 273 | return downLoadInfos; 274 | } 275 | 276 | private static string GetVideoTitle(JObject json) 277 | { 278 | var title = json["args"]["title"]; 279 | 280 | return title == null ? string.Empty : title.ToString(); 281 | } 282 | 283 | private static bool IsVideoUnavailable(string pageSource) 284 | { 285 | const string unavailableString = "
"; 286 | 287 | return pageSource.Contains(unavailableString); 288 | } 289 | 290 | private static JObject LoadJson(string pageSource) 291 | { 292 | var dataRegex = new Regex(@"ytplayer\.config\s*=\s*(\{.+?\});", RegexOptions.Multiline); 293 | 294 | string extractedJson = dataRegex.Match(pageSource).Result("$1"); 295 | 296 | return JObject.Parse(extractedJson); 297 | } 298 | 299 | private static void ThrowYoutubeParseException(Exception innerException, string videoUrl) 300 | { 301 | throw new YoutubeParseException("Could not parse the Youtube page for URL " + videoUrl + "\n" + 302 | "This may be due to a change of the Youtube page structure.\n" + 303 | "Please report this bug at www.github.com/flagbug/YoutubeExtractor/issues", 304 | innerException); 305 | } 306 | 307 | private class ExtractionInfo 308 | { 309 | public bool RequiresDecryption { get; set; } 310 | 311 | public Uri Uri { get; set; } 312 | } 313 | } 314 | } -------------------------------------------------------------------------------- /src/YoutubeExtractorCore.Example/Properties/PublishProfiles/publish-module.psm1: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT MODIFY this file. Visual Studio will override it. 2 | param() 3 | 4 | $script:AspNetPublishHandlers = @{} 5 | 6 | <# 7 | These settings can be overridden with environment variables. 8 | The name of the environment variable should use "Publish" as a 9 | prefix and the names below. For example: 10 | 11 | $env:PublishMSDeployUseChecksum = $true 12 | #> 13 | $global:AspNetPublishSettings = New-Object -TypeName PSCustomObject @{ 14 | MsdeployDefaultProperties = @{ 15 | 'MSDeployUseChecksum'=$false 16 | 'SkipExtraFilesOnServer'=$true 17 | 'retryAttempts' = 20 18 | 'EnableMSDeployBackup' = $false 19 | 'DeleteExistingFiles' = $false 20 | 'AllowUntrustedCertificate'= $false 21 | 'MSDeployPackageContentFoldername'='website\' 22 | 'EnvironmentName' = 'Production' 23 | 'AuthType'='Basic' 24 | 'MSDeployPublishMethod'='WMSVC' 25 | } 26 | } 27 | 28 | function InternalOverrideSettingsFromEnv{ 29 | [cmdletbinding()] 30 | param( 31 | [Parameter(Position=0)] 32 | [object[]]$settings = ($global:AspNetPublishSettings,$global:AspNetPublishSettings.MsdeployDefaultProperties), 33 | 34 | [Parameter(Position=1)] 35 | [string]$prefix = 'Publish' 36 | ) 37 | process{ 38 | foreach($settingsObj in $settings){ 39 | if($settingsObj -eq $null){ 40 | continue 41 | } 42 | 43 | $settingNames = $null 44 | if($settingsObj -is [hashtable]){ 45 | $settingNames = $settingsObj.Keys 46 | } 47 | else{ 48 | $settingNames = ($settingsObj | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) 49 | 50 | } 51 | 52 | foreach($name in @($settingNames)){ 53 | $fullname = ('{0}{1}' -f $prefix,$name) 54 | if(Test-Path "env:$fullname"){ 55 | $settingsObj.$name = ((get-childitem "env:$fullname").Value) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | InternalOverrideSettingsFromEnv -prefix 'Publish' -settings $global:AspNetPublishSettings,$global:AspNetPublishSettings.MsdeployDefaultProperties 63 | 64 | function Register-AspnetPublishHandler{ 65 | [cmdletbinding()] 66 | param( 67 | [Parameter(Mandatory=$true,Position=0)] 68 | $name, 69 | [Parameter(Mandatory=$true,Position=1)] 70 | [ScriptBlock]$handler, 71 | [switch]$force 72 | ) 73 | process{ 74 | if(!($script:AspNetPublishHandlers[$name]) -or $force ){ 75 | 'Adding handler for [{0}]' -f $name | Write-Verbose 76 | $script:AspNetPublishHandlers[$name] = $handler 77 | } 78 | elseif(!($force)){ 79 | 'Ignoring call to Register-AspnetPublishHandler for [name={0}], because a handler with that name exists and -force was not passed.' -f $name | Write-Verbose 80 | } 81 | } 82 | } 83 | 84 | function Get-AspnetPublishHandler{ 85 | [cmdletbinding()] 86 | param( 87 | [Parameter(Mandatory=$true,Position=0)] 88 | $name 89 | ) 90 | process{ 91 | $foundHandler = $script:AspNetPublishHandlers[$name] 92 | 93 | if(!$foundHandler){ 94 | throw ('AspnetPublishHandler with name "{0}" was not found' -f $name) 95 | } 96 | 97 | $foundHandler 98 | } 99 | } 100 | 101 | function GetInternal-ExcludeFilesArg{ 102 | [cmdletbinding()] 103 | param( 104 | $publishProperties 105 | ) 106 | process{ 107 | $excludeFiles = $publishProperties['ExcludeFiles'] 108 | foreach($exclude in $excludeFiles){ 109 | if($exclude){ 110 | [string]$objName = $exclude['objectname'] 111 | 112 | if([string]::IsNullOrEmpty($objName)){ 113 | $objName = 'filePath' 114 | } 115 | 116 | $excludePath = $exclude['absolutepath'] 117 | 118 | # output the result to the return list 119 | ('-skip:objectName={0},absolutePath=''{1}''' -f $objName, $excludePath) 120 | } 121 | } 122 | } 123 | } 124 | 125 | function GetInternal-ReplacementsMSDeployArgs{ 126 | [cmdletbinding()] 127 | param( 128 | $publishProperties 129 | ) 130 | process{ 131 | foreach($replace in ($publishProperties['Replacements'])){ 132 | if($replace){ 133 | $typeValue = $replace['type'] 134 | if(!$typeValue){ $typeValue = 'TextFile' } 135 | 136 | $file = $replace['file'] 137 | $match = $replace['match'] 138 | $newValue = $replace['newValue'] 139 | 140 | if($file -and $match -and $newValue){ 141 | $setParam = ('-setParam:type={0},scope={1},match={2},value={3}' -f $typeValue,$file, $match,$newValue) 142 | 'Adding setparam [{0}]' -f $setParam | Write-Verbose 143 | 144 | # return it 145 | $setParam 146 | } 147 | else{ 148 | 'Skipping replacement because its missing a required value.[file="{0}",match="{1}",newValue="{2}"]' -f $file,$match,$newValue | Write-Verbose 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | <# 156 | .SYNOPSIS 157 | Returns an array of msdeploy arguments that are used across different providers. 158 | For example this will handle useChecksum, AppOffline etc. 159 | This will also add default properties if they are missing. 160 | #> 161 | function GetInternal-SharedMSDeployParametersFrom{ 162 | [cmdletbinding()] 163 | param( 164 | [Parameter(Mandatory=$true,Position=0)] 165 | [HashTable]$publishProperties, 166 | [Parameter(Mandatory=$true,Position=1)] 167 | [System.IO.FileInfo]$packOutput 168 | ) 169 | process{ 170 | $sharedArgs = New-Object psobject -Property @{ 171 | ExtraArgs = @() 172 | DestFragment = '' 173 | EFMigrationData = @{} 174 | } 175 | 176 | # add default properties if they are missing 177 | foreach($propName in $global:AspNetPublishSettings.MsdeployDefaultProperties.Keys){ 178 | if($publishProperties["$propName"] -eq $null){ 179 | $defValue = $global:AspNetPublishSettings.MsdeployDefaultProperties["$propName"] 180 | 'Adding default property to publishProperties ["{0}"="{1}"]' -f $propName,$defValue | Write-Verbose 181 | $publishProperties["$propName"] = $defValue 182 | } 183 | } 184 | 185 | if($publishProperties['MSDeployUseChecksum'] -eq $true){ 186 | $sharedArgs.ExtraArgs += '-usechecksum' 187 | } 188 | 189 | if($publishProperties['EnableMSDeployAppOffline'] -eq $true){ 190 | $sharedArgs.ExtraArgs += '-enablerule:AppOffline' 191 | } 192 | 193 | if($publishProperties['WebPublishMethod'] -eq 'MSDeploy'){ 194 | if($publishProperties['SkipExtraFilesOnServer'] -eq $true){ 195 | $sharedArgs.ExtraArgs += '-enableRule:DoNotDeleteRule' 196 | } 197 | } 198 | 199 | if($publishProperties['WebPublishMethod'] -eq 'FileSystem'){ 200 | if($publishProperties['DeleteExistingFiles'] -eq $false){ 201 | $sharedArgs.ExtraArgs += '-enableRule:DoNotDeleteRule' 202 | } 203 | } 204 | 205 | if($publishProperties['retryAttempts']){ 206 | $sharedArgs.ExtraArgs += ('-retryAttempts:{0}' -f ([int]$publishProperties['retryAttempts'])) 207 | } 208 | 209 | if($publishProperties['EncryptWebConfig'] -eq $true){ 210 | $sharedArgs.ExtraArgs += '-EnableRule:EncryptWebConfig' 211 | } 212 | 213 | if($publishProperties['EnableMSDeployBackup'] -eq $false){ 214 | $sharedArgs.ExtraArgs += '-disablerule:BackupRule' 215 | } 216 | 217 | if($publishProperties['AllowUntrustedCertificate'] -eq $true){ 218 | $sharedArgs.ExtraArgs += '-allowUntrusted' 219 | } 220 | 221 | # add excludes 222 | $sharedArgs.ExtraArgs += (GetInternal-ExcludeFilesArg -publishProperties $publishProperties) 223 | # add replacements 224 | $sharedArgs.ExtraArgs += (GetInternal-ReplacementsMSDeployArgs -publishProperties $publishProperties) 225 | 226 | # add EF Migration 227 | if (($publishProperties['EfMigrations'] -ne $null) -and $publishProperties['EfMigrations'].Count -gt 0){ 228 | if (!(Test-Path -Path $publishProperties['ProjectPath'])) { 229 | throw 'ProjectPath property needs to be defined in the pubxml for EF migration.' 230 | } 231 | try { 232 | # generate T-SQL files 233 | $EFSqlFiles = GenerateInternal-EFMigrationScripts -projectPath $publishProperties['ProjectPath'] -packOutput $packOutput -EFMigrations $publishProperties['EfMigrations'] 234 | $sharedArgs.EFMigrationData.Add('EFSqlFiles',$EFSqlFiles) 235 | } 236 | catch { 237 | throw ('An error occurred while generating EF migrations. {0} {1}' -f $_.Exception,(Get-PSCallStack)) 238 | } 239 | } 240 | # add connection string update 241 | if (($publishProperties['DestinationConnectionStrings'] -ne $null) -and $publishProperties['DestinationConnectionStrings'].Count -gt 0) { 242 | try { 243 | # create/update appsettings.[environment].json 244 | GenerateInternal-AppSettingsFile -packOutput $packOutput -environmentName $publishProperties['EnvironmentName'] -connectionStrings $publishProperties['DestinationConnectionStrings'] 245 | } 246 | catch { 247 | throw ('An error occurred while generating the publish appsettings file. {0} {1}' -f $_.Exception,(Get-PSCallStack)) 248 | } 249 | } 250 | 251 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['ProjectGuid'])) { 252 | AddInternal-ProjectGuidToWebConfig -publishProperties $publishProperties -packOutput $packOutput 253 | } 254 | 255 | # return the args 256 | $sharedArgs 257 | } 258 | } 259 | 260 | <# 261 | .SYNOPSIS 262 | This will publish the folder based on the properties in $publishProperties 263 | 264 | .PARAMETER publishProperties 265 | This is a hashtable containing the publish properties. See the examples here for more info on how to use this parameter. 266 | 267 | .PARAMETER packOutput 268 | The folder path to the output of the dnu publish command. This folder contains the files 269 | that will be published. 270 | 271 | .PARAMETER pubProfilePath 272 | Path to a publish profile (.pubxml file) to import publish properties from. If the same property exists in 273 | publishProperties and the publish profile then publishProperties will win. 274 | 275 | .EXAMPLE 276 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 277 | 'WebPublishMethod'='MSDeploy' 278 | 'MSDeployServiceURL'='contoso.scm.azurewebsites.net:443';` 279 | 'DeployIisAppPath'='contoso';'Username'='$contoso';'Password'="$env:PublishPwd"} 280 | 281 | .EXAMPLE 282 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 283 | 'WebPublishMethod'='FileSystem' 284 | 'publishUrl'="$publishDest" 285 | } 286 | 287 | .EXAMPLE 288 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 289 | 'WebPublishMethod'='MSDeploy' 290 | 'MSDeployServiceURL'='contoso.scm.azurewebsites.net:443';` 291 | 'DeployIisAppPath'='contoso';'Username'='$contoso';'Password'="$env:PublishPwd" 292 | 'ExcludeFiles'=@( 293 | @{'absolutepath'='test.txt'}, 294 | @{'absolutepath'='references.js'} 295 | )} 296 | 297 | .EXAMPLE 298 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 299 | 'WebPublishMethod'='FileSystem' 300 | 'publishUrl'="$publishDest" 301 | 'ExcludeFiles'=@( 302 | @{'absolutepath'='test.txt'}, 303 | @{'absolutepath'='_references.js'}) 304 | 'Replacements' = @( 305 | @{'file'='test.txt$';'match'='REPLACEME';'newValue'='updatedValue'}) 306 | } 307 | 308 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 309 | 'WebPublishMethod'='FileSystem' 310 | 'publishUrl'="$publishDest" 311 | 'ExcludeFiles'=@( 312 | @{'absolutepath'='test.txt'}, 313 | @{'absolutepath'='c:\\full\\path\\ok\\as\\well\\_references.js'}) 314 | 'Replacements' = @( 315 | @{'file'='test.txt$';'match'='REPLACEME';'newValue'='updatedValue'}) 316 | } 317 | 318 | .EXAMPLE 319 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 320 | 'WebPublishMethod'='FileSystem' 321 | 'publishUrl'="$publishDest" 322 | 'EnableMSDeployAppOffline'='true' 323 | 'AppOfflineTemplate'='offline-template.html' 324 | 'MSDeployUseChecksum'='true' 325 | } 326 | #> 327 | function Publish-AspNet{ 328 | param( 329 | [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 330 | [hashtable]$publishProperties = @{}, 331 | 332 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 333 | [System.IO.FileInfo]$packOutput, 334 | 335 | [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)] 336 | [System.IO.FileInfo]$pubProfilePath 337 | ) 338 | process{ 339 | if($publishProperties['WebPublishMethodOverride']){ 340 | 'Overriding publish method from $publishProperties[''WebPublishMethodOverride''] to [{0}]' -f ($publishProperties['WebPublishMethodOverride']) | Write-Verbose 341 | $publishProperties['WebPublishMethod'] = $publishProperties['WebPublishMethodOverride'] 342 | } 343 | 344 | if(-not [string]::IsNullOrWhiteSpace($pubProfilePath)){ 345 | $profileProperties = Get-PropertiesFromPublishProfile -filepath $pubProfilePath 346 | foreach($key in $profileProperties.Keys){ 347 | if(-not ($publishProperties.ContainsKey($key))){ 348 | 'Adding properties from publish profile [''{0}''=''{1}'']' -f $key,$profileProperties[$key] | Write-Verbose 349 | $publishProperties.Add($key,$profileProperties[$key]) 350 | } 351 | } 352 | } 353 | 354 | if(!([System.IO.Path]::IsPathRooted($packOutput))){ 355 | $packOutput = [System.IO.Path]::GetFullPath((Join-Path $pwd $packOutput)) 356 | } 357 | 358 | $pubMethod = $publishProperties['WebPublishMethod'] 359 | 'Publishing with publish method [{0}]' -f $pubMethod | Write-Output 360 | 361 | # get the handler based on WebPublishMethod, and call it. 362 | &(Get-AspnetPublishHandler -name $pubMethod) $publishProperties $packOutput 363 | } 364 | } 365 | 366 | <# 367 | .SYNOPSIS 368 | 369 | Inputs: 370 | 371 | Example of $xmlDocument: '' 372 | Example of $providerDataArray: 373 | 374 | [System.Collections.ArrayList]$providerDataArray = @() 375 | 376 | $iisAppSourceKeyValue=@{"iisApp" = @{"path"='c:\temp\pathtofiles';"appOfflineTemplate" ='offline-template.html'}} 377 | $providerDataArray.Add($iisAppSourceKeyValue) 378 | 379 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile"}} 380 | $providerDataArray.Add($dbfullsqlKeyValue) 381 | 382 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile2"}} 383 | $providerDataArray.Add($dbfullsqlKeyValue) 384 | 385 | Manifest File content: 386 | 387 | 388 | 389 | 390 | 391 | 392 | #> 393 | function AddInternal-ProviderDataToManifest { 394 | [cmdletbinding()] 395 | param( 396 | [Parameter(Mandatory=$true, Position=0)] 397 | [XML]$xmlDocument, 398 | [Parameter(Position=1)] 399 | [System.Collections.ArrayList]$providerDataArray 400 | ) 401 | process { 402 | $siteNode = $xmlDocument.SelectSingleNode("/sitemanifest") 403 | if ($siteNode -eq $null) { 404 | throw 'sitemanifest element is missing in the xml object' 405 | } 406 | foreach ($providerData in $providerDataArray) { 407 | foreach ($providerName in $providerData.Keys) { 408 | $providerValue = $providerData[$providerName] 409 | $xmlNode = $xmlDocument.CreateElement($providerName) 410 | foreach ($providerValueKey in $providerValue.Keys) { 411 | $xmlNode.SetAttribute($providerValueKey, $providerValue[$providerValueKey]) | Out-Null 412 | } 413 | $siteNode.AppendChild($xmlNode) | Out-Null 414 | } 415 | } 416 | } 417 | } 418 | 419 | function AddInternal-ProjectGuidToWebConfig { 420 | [cmdletbinding()] 421 | param( 422 | [Parameter(Position=0)] 423 | [HashTable]$publishProperties, 424 | [Parameter(Position=1)] 425 | [System.IO.FileInfo]$packOutput 426 | ) 427 | process { 428 | try { 429 | [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") | Out-Null 430 | $webConfigPath = Join-Path $packOutput 'web.config' 431 | $projectGuidCommentValue = 'ProjectGuid: {0}' -f $publishProperties['ProjectGuid'] 432 | $xDoc = [System.Xml.Linq.XDocument]::Load($webConfigPath) 433 | $allNodes = $xDoc.DescendantNodes() 434 | $projectGuidComment = $allNodes | Where-Object { $_.NodeType -eq [System.Xml.XmlNodeType]::Comment -and $_.Value -eq $projectGuidCommentValue } | Select -First 1 435 | if($projectGuidComment -ne $null) { 436 | if($publishProperties['IgnoreProjectGuid'] -eq $true) { 437 | $projectGuidComment.Remove() | Out-Null 438 | $xDoc.Save($webConfigPath) | Out-Null 439 | } 440 | } 441 | else { 442 | if(-not ($publishProperties['IgnoreProjectGuid'] -eq $true)) { 443 | $projectGuidComment = New-Object -TypeName System.Xml.Linq.XComment -ArgumentList $projectGuidCommentValue 444 | $xDoc.LastNode.AddAfterSelf($projectGuidComment) | Out-Null 445 | $xDoc.Save($webConfigPath) | Out-Null 446 | } 447 | } 448 | } 449 | catch { 450 | } 451 | } 452 | } 453 | 454 | <# 455 | .SYNOPSIS 456 | 457 | Example of $EFMigrations: 458 | $EFMigrations = @{'CarContext'='Car Context ConnectionString';'MovieContext'='Movie Context Connection String'} 459 | 460 | #> 461 | 462 | function GenerateInternal-EFMigrationScripts { 463 | [cmdletbinding()] 464 | param( 465 | [Parameter(Mandatory=$true,Position=0)] 466 | [System.IO.FileInfo]$projectPath, 467 | [Parameter(Mandatory=$true,Position=1)] 468 | [System.IO.FileInfo]$packOutput, 469 | [Parameter(Position=2)] 470 | [HashTable]$EFMigrations 471 | ) 472 | process { 473 | $files = @{} 474 | $dotnetExePath = GetInternal-DotNetExePath 475 | foreach ($dbContextName in $EFMigrations.Keys) { 476 | try 477 | { 478 | $tempDir = GetInternal-PublishTempPath -packOutput $packOutput 479 | $efScriptFile = Join-Path $tempDir ('{0}.sql' -f $dbContextName) 480 | $arg = ('ef migrations script --idempotent --output {0} --context {1}' -f 481 | $efScriptFile, 482 | $dbContextName) 483 | 484 | Execute-Command $dotnetExePath $arg $projectPath | Out-Null 485 | if (Test-Path -Path $efScriptFile) { 486 | if (!($files.ContainsKey($dbContextName))) { 487 | $files.Add($dbContextName, $efScriptFile) | Out-Null 488 | } 489 | } 490 | } 491 | catch 492 | { 493 | throw 'error occured when executing dotnet.exe to generate EF T-SQL file' 494 | } 495 | } 496 | # return files object 497 | $files 498 | } 499 | } 500 | 501 | <# 502 | .SYNOPSIS 503 | 504 | Example of $connectionStrings: 505 | $connectionStrings = @{'DefaultConnection'='Default ConnectionString';'CarConnection'='Car Connection String'} 506 | 507 | #> 508 | function GenerateInternal-AppSettingsFile { 509 | [cmdletbinding()] 510 | param( 511 | [Parameter(Mandatory = $true,Position=0)] 512 | [System.IO.FileInfo]$packOutput, 513 | [Parameter(Mandatory = $true,Position=1)] 514 | [string]$environmentName, 515 | [Parameter(Position=2)] 516 | [HashTable]$connectionStrings 517 | ) 518 | process { 519 | $configProdJsonFile = 'appsettings.{0}.json' -f $environmentName 520 | $configProdJsonFilePath = Join-Path -Path $packOutput -ChildPath $configProdJsonFile 521 | 522 | if ([string]::IsNullOrEmpty($configProdJsonFilePath)) { 523 | throw ('The path of {0} is empty' -f $configProdJsonFilePath) 524 | } 525 | 526 | if(!(Test-Path -Path $configProdJsonFilePath)) { 527 | # create new file 528 | '{}' | out-file -encoding utf8 -filePath $configProdJsonFilePath -Force 529 | } 530 | 531 | $jsonObj = ConvertFrom-Json -InputObject (Get-Content -Path $configProdJsonFilePath -Raw) 532 | # update when there exists one or more connection strings 533 | if ($connectionStrings -ne $null) { 534 | foreach ($name in $connectionStrings.Keys) { 535 | #check for hierarchy style 536 | if ($jsonObj.ConnectionStrings.$name) { 537 | $jsonObj.ConnectionStrings.$name = $connectionStrings[$name] 538 | continue 539 | } 540 | #check for horizontal style 541 | $horizontalName = 'ConnectionStrings.{0}:' -f $name 542 | if ($jsonObj.$horizontalName) { 543 | $jsonObj.$horizontalName = $connectionStrings[$name] 544 | continue 545 | } 546 | # create new one 547 | if (!($jsonObj.ConnectionStrings)) { 548 | $contentForDefaultConnection = '{}' 549 | $jsonObj | Add-Member -name 'ConnectionStrings' -value (ConvertFrom-Json -InputObject $contentForDefaultConnection) -MemberType NoteProperty | Out-Null 550 | } 551 | if (!($jsonObj.ConnectionStrings.$name)) { 552 | $jsonObj.ConnectionStrings | Add-Member -name $name -value $connectionStrings[$name] -MemberType NoteProperty | Out-Null 553 | } 554 | } 555 | } 556 | 557 | $jsonObj | ConvertTo-Json | out-file -encoding utf8 -filePath $configProdJsonFilePath -Force 558 | 559 | #return the path of config.[environment].json 560 | $configProdJsonFilePath 561 | } 562 | } 563 | 564 | <# 565 | .SYNOPSIS 566 | 567 | Inputs: 568 | Example of $providerDataArray: 569 | 570 | [System.Collections.ArrayList]$providerDataArray = @() 571 | 572 | $iisAppSourceKeyValue=@{"iisApp" = @{"path"='c:\temp\pathtofiles';"appOfflineTemplate" ='offline-template.html'}} 573 | $providerDataArray.Add($iisAppSourceKeyValue) 574 | 575 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile"}} 576 | $providerDataArray.Add($dbfullsqlKeyValue) 577 | 578 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile2"}} 579 | $providerDataArray.Add($dbfullsqlKeyValue) 580 | 581 | Manifest File content: 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | #> 590 | 591 | function GenerateInternal-ManifestFile { 592 | [cmdletbinding()] 593 | param( 594 | [Parameter(Mandatory=$true,Position=0)] 595 | [System.IO.FileInfo]$packOutput, 596 | [Parameter(Mandatory=$true,Position=1)] 597 | $publishProperties, 598 | [Parameter(Mandatory=$true,Position=2)] 599 | [System.Collections.ArrayList]$providerDataArray, 600 | [Parameter(Mandatory=$true,Position=3)] 601 | [ValidateNotNull()] 602 | $manifestFileName 603 | ) 604 | process{ 605 | $xmlDocument = [xml]'' 606 | AddInternal-ProviderDataToManifest -xmlDocument $xmlDocument -providerDataArray $providerDataArray | Out-Null 607 | $publishTempDir = GetInternal-PublishTempPath -packOutput $packOutput 608 | $XMLFile = Join-Path $publishTempDir $manifestFileName 609 | $xmlDocument.OuterXml | out-file -encoding utf8 -filePath $XMLFile -Force 610 | 611 | # return 612 | [System.IO.FileInfo]$XMLFile 613 | } 614 | } 615 | 616 | function GetInternal-PublishTempPath { 617 | [cmdletbinding()] 618 | param( 619 | [Parameter(Mandatory=$true, Position=0)] 620 | [System.IO.FileInfo]$packOutput 621 | ) 622 | process { 623 | $tempDir = [io.path]::GetTempPath() 624 | $packOutputFolderName = Split-Path $packOutput -Leaf 625 | $publishTempDir = [io.path]::combine($tempDir,'PublishTemp','obj',$packOutputFolderName) 626 | if (!(Test-Path -Path $publishTempDir)) { 627 | New-Item -Path $publishTempDir -type directory | Out-Null 628 | } 629 | # return 630 | [System.IO.FileInfo]$publishTempDir 631 | } 632 | } 633 | 634 | function Publish-AspNetMSDeploy{ 635 | param( 636 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 637 | $publishProperties, 638 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 639 | $packOutput 640 | ) 641 | process{ 642 | if($publishProperties){ 643 | $publishPwd = $publishProperties['Password'] 644 | 645 | $sharedArgs = GetInternal-SharedMSDeployParametersFrom -publishProperties $publishProperties -packOutput $packOutput 646 | $iisAppPath = $publishProperties['DeployIisAppPath'] 647 | 648 | # create source manifest 649 | 650 | # e.g 651 | # 652 | # 653 | # 654 | # 655 | # 656 | # 657 | 658 | [System.Collections.ArrayList]$providerDataArray = @() 659 | $iisAppValues = @{"path"=$packOutput}; 660 | $iisAppSourceKeyValue=@{"iisApp" = $iisAppValues} 661 | $providerDataArray.Add($iisAppSourceKeyValue) | Out-Null 662 | 663 | if ($sharedArgs.EFMigrationData -ne $null -and $sharedArgs.EFMigrationData.Contains('EFSqlFiles')) { 664 | foreach ($sqlFile in $sharedArgs.EFMigrationData['EFSqlFiles'].Values) { 665 | $dbFullSqlSourceKeyValue=@{"dbFullSql" = @{"path"=$sqlFile}} 666 | $providerDataArray.Add($dbFullSqlSourceKeyValue) | Out-Null 667 | } 668 | } 669 | 670 | [System.IO.FileInfo]$sourceXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'SourceManifest.xml' 671 | 672 | $providerDataArray.Clear() | Out-Null 673 | # create destination manifest 674 | 675 | # e.g 676 | # 677 | # 678 | # 679 | # 680 | # 681 | 682 | $iisAppValues = @{"path"=$iisAppPath}; 683 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['AppOfflineTemplate'])){ 684 | $iisAppValues.Add("appOfflineTemplate", $publishProperties['AppOfflineTemplate']) | Out-Null 685 | } 686 | 687 | $iisAppDestinationKeyValue=@{"iisApp" = $iisAppValues} 688 | $providerDataArray.Add($iisAppDestinationKeyValue) | Out-Null 689 | 690 | if ($publishProperties['EfMigrations'] -ne $null -and $publishProperties['EfMigrations'].Count -gt 0) { 691 | foreach ($connectionString in $publishProperties['EfMigrations'].Values) { 692 | $dbFullSqlDestinationKeyValue=@{"dbFullSql" = @{"path"=$connectionString}} 693 | $providerDataArray.Add($dbFullSqlDestinationKeyValue) | Out-Null 694 | } 695 | } 696 | 697 | 698 | [System.IO.FileInfo]$destXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'DestinationManifest.xml' 699 | 700 | <# 701 | "C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe" 702 | -source:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\SourceManifest.xml' 703 | -dest:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\DestManifest.xml',ComputerName='https://contoso.scm.azurewebsites.net/msdeploy.axd',UserName='$contoso',Password='',IncludeAcls='False',AuthType='Basic' 704 | -verb:sync 705 | -enableRule:DoNotDeleteRule 706 | -retryAttempts=2" 707 | #> 708 | 709 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['MSDeployPublishMethod'])){ 710 | $serviceMethod = $publishProperties['MSDeployPublishMethod'] 711 | } 712 | 713 | $msdeployComputerName= InternalNormalize-MSDeployUrl -serviceUrl $publishProperties['MSDeployServiceURL'] -siteName $iisAppPath -serviceMethod $publishProperties['MSDeployPublishMethod'] 714 | if($publishProperties['UseMSDeployServiceURLAsIs'] -eq $true){ 715 | $msdeployComputerName = $publishProperties['MSDeployServiceURL'] 716 | } 717 | 718 | $publishArgs = @() 719 | #use manifest to publish 720 | $publishArgs += ('-source:manifest=''{0}''' -f $sourceXMLFile.FullName) 721 | $publishArgs += ('-dest:manifest=''{0}'',ComputerName=''{1}'',UserName=''{2}'',Password=''{3}'',IncludeAcls=''False'',AuthType=''{4}''{5}' -f 722 | $destXMLFile.FullName, 723 | $msdeployComputerName, 724 | $publishProperties['UserName'], 725 | $publishPwd, 726 | $publishProperties['AuthType'], 727 | $sharedArgs.DestFragment) 728 | $publishArgs += '-verb:sync' 729 | $publishArgs += $sharedArgs.ExtraArgs 730 | 731 | $command = '"{0}" {1}' -f (Get-MSDeploy),($publishArgs -join ' ') 732 | 733 | if (! [String]::IsNullOrEmpty($publishPwd)) { 734 | $command.Replace($publishPwd,'{PASSWORD-REMOVED-FROM-LOG}') | Print-CommandString 735 | } 736 | Execute-Command -exePath (Get-MSDeploy) -arguments ($publishArgs -join ' ') 737 | } 738 | else{ 739 | throw 'publishProperties is empty, cannot publish' 740 | } 741 | } 742 | } 743 | 744 | function Escape-TextForRegularExpressions{ 745 | [cmdletbinding()] 746 | param( 747 | [Parameter(Position=0,Mandatory=$true)] 748 | [string]$text 749 | ) 750 | process{ 751 | [regex]::Escape($text) 752 | } 753 | } 754 | 755 | function Publish-AspNetMSDeployPackage{ 756 | param( 757 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 758 | $publishProperties, 759 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 760 | $packOutput 761 | ) 762 | process{ 763 | if($publishProperties){ 764 | $packageDestinationFilepath = $publishProperties['DesktopBuildPackageLocation'] 765 | 766 | if(!$packageDestinationFilepath){ 767 | throw ('The package destination property (DesktopBuildPackageLocation) was not found in the publish properties') 768 | } 769 | 770 | if(!([System.IO.Path]::IsPathRooted($packageDestinationFilepath))){ 771 | $packageDestinationFilepath = [System.IO.Path]::GetFullPath((Join-Path $pwd $packageDestinationFilepath)) 772 | } 773 | 774 | # if the dir doesn't exist create it 775 | $pkgDir = ((new-object -typename System.IO.FileInfo($packageDestinationFilepath)).Directory) 776 | if(!(Test-Path -Path $pkgDir)) { 777 | New-Item $pkgDir -type Directory | Out-Null 778 | } 779 | 780 | <# 781 | "C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe" 782 | -source:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\SourceManifest.xml' 783 | -dest:package=c:\temp\path\contosoweb.zip 784 | -verb:sync 785 | -enableRule:DoNotDeleteRule 786 | -retryAttempts=2 787 | #> 788 | 789 | $sharedArgs = GetInternal-SharedMSDeployParametersFrom -publishProperties $publishProperties -packOutput $packOutput 790 | 791 | # create source manifest 792 | 793 | # e.g 794 | # 795 | # 796 | # 797 | # 798 | 799 | [System.Collections.ArrayList]$providerDataArray = @() 800 | $iisAppSourceKeyValue=@{"iisApp" = @{"path"=$packOutput}} 801 | $providerDataArray.Add($iisAppSourceKeyValue) | Out-Null 802 | 803 | [System.IO.FileInfo]$sourceXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'SourceManifest.xml' 804 | 805 | $publishArgs = @() 806 | $publishArgs += ('-source:manifest=''{0}''' -f $sourceXMLFile.FullName) 807 | $publishArgs += ('-dest:package=''{0}''' -f $packageDestinationFilepath) 808 | $publishArgs += '-verb:sync' 809 | $packageContentFolder = $publishProperties['MSDeployPackageContentFoldername'] 810 | if(!$packageContentFolder){ $packageContentFolder = 'website' } 811 | $publishArgs += ('-replace:match=''{0}'',replace=''{1}''' -f (Escape-TextForRegularExpressions $packOutput), $packageContentFolder ) 812 | $publishArgs += $sharedArgs.ExtraArgs 813 | 814 | $command = '"{0}" {1}' -f (Get-MSDeploy),($publishArgs -join ' ') 815 | $command | Print-CommandString 816 | Execute-Command -exePath (Get-MSDeploy) -arguments ($publishArgs -join ' ') 817 | } 818 | else{ 819 | throw 'publishProperties is empty, cannot publish' 820 | } 821 | } 822 | } 823 | 824 | function Publish-AspNetFileSystem{ 825 | param( 826 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 827 | $publishProperties, 828 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 829 | $packOutput 830 | ) 831 | process{ 832 | $pubOut = $publishProperties['publishUrl'] 833 | 834 | if([string]::IsNullOrWhiteSpace($pubOut)){ 835 | throw ('publishUrl is a required property for FileSystem publish but it was empty.') 836 | } 837 | 838 | # if it's a relative path then update it to a full path 839 | if(!([System.IO.Path]::IsPathRooted($pubOut))){ 840 | $pubOut = [System.IO.Path]::GetFullPath((Join-Path $pwd $pubOut)) 841 | $publishProperties['publishUrl'] = "$pubOut" 842 | } 843 | 844 | 'Publishing files to {0}' -f $pubOut | Write-Output 845 | 846 | # we use msdeploy.exe because it supports incremental publish/skips/replacements/etc 847 | # msdeploy.exe -verb:sync -source:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\SourceManifest.xml' -dest:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\DestManifest.xml' 848 | 849 | $sharedArgs = GetInternal-SharedMSDeployParametersFrom -publishProperties $publishProperties -packOutput $packOutput 850 | 851 | # create source manifest 852 | 853 | # e.g 854 | # 855 | # 856 | # 857 | # 858 | 859 | [System.Collections.ArrayList]$providerDataArray = @() 860 | $contentPathValues = @{"path"=$packOutput}; 861 | $contentPathSourceKeyValue=@{"contentPath" = $contentPathValues} 862 | $providerDataArray.Add($contentPathSourceKeyValue) | Out-Null 863 | 864 | [System.IO.FileInfo]$sourceXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'SourceManifest.xml' 865 | 866 | $providerDataArray.Clear() | Out-Null 867 | # create destination manifest 868 | 869 | # e.g 870 | # 871 | # 872 | # 873 | $contentPathValues = @{"path"=$publishProperties['publishUrl']}; 874 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['AppOfflineTemplate'])){ 875 | $contentPathValues.Add("appOfflineTemplate", $publishProperties['AppOfflineTemplate']) | Out-Null 876 | } 877 | $contentPathDestinationKeyValue=@{"contentPath" = $contentPathValues} 878 | $providerDataArray.Add($contentPathDestinationKeyValue) | Out-Null 879 | 880 | [System.IO.FileInfo]$destXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'DestinationManifest.xml' 881 | 882 | $publishArgs = @() 883 | $publishArgs += ('-source:manifest=''{0}''' -f $sourceXMLFile.FullName) 884 | $publishArgs += ('-dest:manifest=''{0}''{1}' -f $destXMLFile.FullName, $sharedArgs.DestFragment) 885 | $publishArgs += '-verb:sync' 886 | $publishArgs += $sharedArgs.ExtraArgs 887 | 888 | $command = '"{0}" {1}' -f (Get-MSDeploy),($publishArgs -join ' ') 889 | $command | Print-CommandString 890 | Execute-Command -exePath (Get-MSDeploy) -arguments ($publishArgs -join ' ') 891 | 892 | # copy sql script to script folder 893 | if (($sharedArgs.EFMigrationData['EFSqlFiles'] -ne $null) -and ($sharedArgs.EFMigrationData['EFSqlFiles'].Count -gt 0)) { 894 | $scriptsDir = Join-Path $pubOut 'efscripts' 895 | 896 | if (!(Test-Path -Path $scriptsDir)) { 897 | New-Item -Path $scriptsDir -type directory | Out-Null 898 | } 899 | 900 | foreach ($sqlFile in $sharedArgs.EFMigrationData['EFSqlFiles'].Values) { 901 | Copy-Item $sqlFile -Destination $scriptsDir -Force -Recurse | Out-Null 902 | } 903 | } 904 | } 905 | } 906 | 907 | <# 908 | .SYNOPSIS 909 | This can be used to read a publish profile to extract the property values into a hashtable. 910 | 911 | .PARAMETER filepath 912 | Path to the publish profile to get the properties from. Currenlty this only supports reading 913 | .pubxml files. 914 | 915 | .EXAMPLE 916 | Get-PropertiesFromPublishProfile -filepath c:\projects\publish\devpublish.pubxml 917 | #> 918 | function Get-PropertiesFromPublishProfile{ 919 | [cmdletbinding()] 920 | param( 921 | [Parameter(Position=0,Mandatory=$true)] 922 | [ValidateNotNull()] 923 | [ValidateScript({Test-Path $_})] 924 | [System.IO.FileInfo]$filepath 925 | ) 926 | begin{ 927 | Add-Type -AssemblyName System.Core 928 | Add-Type -AssemblyName Microsoft.Build 929 | } 930 | process{ 931 | 'Reading publish properties from profile [{0}]' -f $filepath | Write-Verbose 932 | # use MSBuild to get the project and read properties 933 | $projectCollection = (New-Object Microsoft.Build.Evaluation.ProjectCollection) 934 | if(!([System.IO.Path]::IsPathRooted($filepath))){ 935 | $filepath = [System.IO.Path]::GetFullPath((Join-Path $pwd $filepath)) 936 | } 937 | $project = ([Microsoft.Build.Construction.ProjectRootElement]::Open([string]$filepath.Fullname, $projectCollection)) 938 | 939 | $properties = @{} 940 | foreach($property in $project.Properties){ 941 | $properties[$property.Name]=$property.Value 942 | } 943 | 944 | $properties 945 | } 946 | } 947 | 948 | function Print-CommandString{ 949 | [cmdletbinding()] 950 | param( 951 | [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] 952 | $command 953 | ) 954 | process{ 955 | 'Executing command [{0}]' -f $command | Write-Output 956 | } 957 | } 958 | 959 | function Execute-CommandString{ 960 | [cmdletbinding()] 961 | param( 962 | [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] 963 | [string[]]$command, 964 | 965 | [switch] 966 | $useInvokeExpression, 967 | 968 | [switch] 969 | $ignoreErrors 970 | ) 971 | process{ 972 | foreach($cmdToExec in $command){ 973 | 'Executing command [{0}]' -f $cmdToExec | Write-Verbose 974 | if($useInvokeExpression){ 975 | try { 976 | Invoke-Expression -Command $cmdToExec 977 | } 978 | catch { 979 | if(-not $ignoreErrors){ 980 | $msg = ('The command [{0}] exited with exception [{1}]' -f $cmdToExec, $_.ToString()) 981 | throw $msg 982 | } 983 | } 984 | } 985 | else { 986 | cmd.exe /D /C $cmdToExec 987 | 988 | if(-not $ignoreErrors -and ($LASTEXITCODE -ne 0)){ 989 | $msg = ('The command [{0}] exited with code [{1}]' -f $cmdToExec, $LASTEXITCODE) 990 | throw $msg 991 | } 992 | } 993 | } 994 | } 995 | } 996 | 997 | function Execute-Command { 998 | [cmdletbinding()] 999 | param( 1000 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1001 | [String]$exePath, 1002 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1003 | [String]$arguments, 1004 | [Parameter(Position=2)] 1005 | [System.IO.FileInfo]$workingDirectory 1006 | ) 1007 | process{ 1008 | $psi = New-Object -TypeName System.Diagnostics.ProcessStartInfo 1009 | $psi.CreateNoWindow = $true 1010 | $psi.UseShellExecute = $false 1011 | $psi.RedirectStandardOutput = $true 1012 | $psi.RedirectStandardError=$true 1013 | $psi.FileName = $exePath 1014 | $psi.Arguments = $arguments 1015 | if($workingDirectory -and (Test-Path -Path $workingDirectory)) { 1016 | $psi.WorkingDirectory = $workingDirectory 1017 | } 1018 | 1019 | $process = New-Object -TypeName System.Diagnostics.Process 1020 | $process.StartInfo = $psi 1021 | $process.EnableRaisingEvents=$true 1022 | 1023 | # Register the event handler for error 1024 | $stdErrEvent = Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -Action { 1025 | if (! [String]::IsNullOrEmpty($EventArgs.Data)) { 1026 | $EventArgs.Data | Write-Error 1027 | } 1028 | } 1029 | 1030 | # Starting process. 1031 | $process.Start() | Out-Null 1032 | $process.BeginErrorReadLine() | Out-Null 1033 | $output = $process.StandardOutput.ReadToEnd() 1034 | $process.WaitForExit() | Out-Null 1035 | $output | Write-Output 1036 | 1037 | # UnRegister the event handler for error 1038 | Unregister-Event -SourceIdentifier $stdErrEvent.Name | Out-Null 1039 | } 1040 | } 1041 | 1042 | 1043 | function GetInternal-DotNetExePath { 1044 | process { 1045 | $dotnetinstallpath = $env:dotnetinstallpath 1046 | if (!$dotnetinstallpath) { 1047 | $DotNetRegItem = Get-ItemProperty -Path 'hklm:\software\dotnet\setup\' 1048 | if ($env:DOTNET_HOME) { 1049 | $dotnetinstallpath = Join-Path $env:DOTNET_HOME -ChildPath 'dotnet.exe' 1050 | } 1051 | elseif ($DotNetRegItem -and $DotNetRegItem.InstallDir){ 1052 | $dotnetinstallpath = Join-Path $DotNetRegItem.InstallDir -ChildPath 'dotnet.exe' 1053 | } 1054 | } 1055 | if (!(Test-Path $dotnetinstallpath)) { 1056 | throw 'Unable to find dotnet.exe, please install it and try again' 1057 | } 1058 | # return 1059 | [System.IO.FileInfo]$dotnetinstallpath 1060 | } 1061 | } 1062 | 1063 | function Get-MSDeploy{ 1064 | [cmdletbinding()] 1065 | param() 1066 | process{ 1067 | $installPath = $env:msdeployinstallpath 1068 | 1069 | if(!$installPath){ 1070 | $keysToCheck = @('hklm:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\3','hklm:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\2','hklm:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\1') 1071 | 1072 | foreach($keyToCheck in $keysToCheck){ 1073 | if(Test-Path $keyToCheck){ 1074 | $installPath = (Get-itemproperty $keyToCheck -Name InstallPath -ErrorAction SilentlyContinue | select -ExpandProperty InstallPath -ErrorAction SilentlyContinue) 1075 | } 1076 | 1077 | if($installPath){ 1078 | break; 1079 | } 1080 | } 1081 | } 1082 | 1083 | if(!$installPath){ 1084 | throw "Unable to find msdeploy.exe, please install it and try again" 1085 | } 1086 | 1087 | [string]$msdInstallLoc = (join-path $installPath 'msdeploy.exe') 1088 | 1089 | "Found msdeploy.exe at [{0}]" -f $msdInstallLoc | Write-Verbose 1090 | 1091 | $msdInstallLoc 1092 | } 1093 | } 1094 | 1095 | function InternalNormalize-MSDeployUrl{ 1096 | [cmdletbinding()] 1097 | param( 1098 | [Parameter(Position=0,Mandatory=$true)] 1099 | [string]$serviceUrl, 1100 | 1101 | [string] $siteName, 1102 | 1103 | [ValidateSet('WMSVC','RemoteAgent','InProc')] 1104 | [string]$serviceMethod = 'WMSVC' 1105 | ) 1106 | process{ 1107 | $tempUrl = $serviceUrl 1108 | $resultUrl = $serviceUrl 1109 | 1110 | $httpsStr = 'https://' 1111 | $httpStr = 'http://' 1112 | $msdeployAxd = 'msdeploy.axd' 1113 | 1114 | if(-not [string]::IsNullOrWhiteSpace($serviceUrl)){ 1115 | if([string]::Compare($serviceMethod,'WMSVC',[StringComparison]::OrdinalIgnoreCase) -eq 0){ 1116 | # if no http or https then add one 1117 | if(-not ($serviceUrl.StartsWith($httpStr,[StringComparison]::OrdinalIgnoreCase) -or 1118 | $serviceUrl.StartsWith($httpsStr,[StringComparison]::OrdinalIgnoreCase)) ){ 1119 | 1120 | $serviceUrl = [string]::Concat($httpsStr,$serviceUrl.TrimStart()) 1121 | } 1122 | [System.Uri]$serviceUri = New-Object -TypeName 'System.Uri' $serviceUrl 1123 | [System.UriBuilder]$serviceUriBuilder = New-Object -TypeName 'System.UriBuilder' $serviceUrl 1124 | 1125 | # if it's https and the port was not passed in override it to 8172 1126 | if( ([string]::Compare('https',$serviceUriBuilder.Scheme,[StringComparison]::OrdinalIgnoreCase) -eq 0) -and 1127 | -not $serviceUrl.Contains((':{0}' -f $serviceUriBuilder.Port)) ) { 1128 | $serviceUriBuilder.Port = 8172 1129 | } 1130 | 1131 | # if no path then add one 1132 | if([string]::Compare('/',$serviceUriBuilder.Path,[StringComparison]::OrdinalIgnoreCase) -eq 0){ 1133 | $serviceUriBuilder.Path = $msdeployAxd 1134 | } 1135 | 1136 | if ([string]::IsNullOrEmpty($serviceUriBuilder.Query) -and -not([string]::IsNullOrEmpty($siteName))) 1137 | { 1138 | $serviceUriBuilder.Query = "site=" + $siteName; 1139 | } 1140 | 1141 | $resultUrl = $serviceUriBuilder.Uri.AbsoluteUri 1142 | } 1143 | elseif([string]::Compare($serviceMethod,'RemoteAgent',[StringComparison]::OrdinalIgnoreCase) -eq 0){ 1144 | [System.UriBuilder]$serviceUriBuilder = New-Object -TypeName 'System.UriBuilder' $serviceUrl 1145 | # http://{computername}/MSDEPLOYAGENTSERVICE 1146 | # remote agent must use http 1147 | $serviceUriBuilder.Scheme = 'http' 1148 | $serviceUriBuilder.Path = '/MSDEPLOYAGENTSERVICE' 1149 | 1150 | $resultUrl = $serviceUriBuilder.Uri.AbsoluteUri 1151 | } 1152 | else{ 1153 | # see if it's for localhost 1154 | [System.Uri]$serviceUri = New-Object -TypeName 'System.Uri' $serviceUrl 1155 | $resultUrl = $serviceUri.AbsoluteUri 1156 | } 1157 | } 1158 | 1159 | # return the result to the caller 1160 | $resultUrl 1161 | } 1162 | } 1163 | 1164 | function InternalRegister-AspNetKnownPublishHandlers{ 1165 | [cmdletbinding()] 1166 | param() 1167 | process{ 1168 | 'Registering MSDeploy handler' | Write-Verbose 1169 | Register-AspnetPublishHandler -name 'MSDeploy' -force -handler { 1170 | [cmdletbinding()] 1171 | param( 1172 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1173 | $publishProperties, 1174 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1175 | $packOutput 1176 | ) 1177 | 1178 | Publish-AspNetMSDeploy -publishProperties $publishProperties -packOutput $packOutput 1179 | } 1180 | 1181 | 'Registering MSDeploy package handler' | Write-Verbose 1182 | Register-AspnetPublishHandler -name 'Package' -force -handler { 1183 | [cmdletbinding()] 1184 | param( 1185 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1186 | $publishProperties, 1187 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1188 | $packOutput 1189 | ) 1190 | 1191 | Publish-AspNetMSDeployPackage -publishProperties $publishProperties -packOutput $packOutput 1192 | } 1193 | 1194 | 'Registering FileSystem handler' | Write-Verbose 1195 | Register-AspnetPublishHandler -name 'FileSystem' -force -handler { 1196 | [cmdletbinding()] 1197 | param( 1198 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1199 | $publishProperties, 1200 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1201 | $packOutput 1202 | ) 1203 | 1204 | Publish-AspNetFileSystem -publishProperties $publishProperties -packOutput $packOutput 1205 | } 1206 | } 1207 | } 1208 | 1209 | <# 1210 | .SYNOPSIS 1211 | Used for testing purposes only. 1212 | #> 1213 | function InternalReset-AspNetPublishHandlers{ 1214 | [cmdletbinding()] 1215 | param() 1216 | process{ 1217 | $script:AspNetPublishHandlers = @{} 1218 | InternalRegister-AspNetKnownPublishHandlers 1219 | } 1220 | } 1221 | 1222 | Export-ModuleMember -function Get-*,Publish-*,Register-*,Enable-* 1223 | if($env:IsDeveloperMachine){ 1224 | # you can set the env var to expose all functions to importer. easy for development. 1225 | # this is required for executing pester test cases, it's set by build.ps1 1226 | Export-ModuleMember -function * 1227 | } 1228 | 1229 | # register the handlers so that Publish-AspNet can be called 1230 | InternalRegister-AspNetKnownPublishHandlers 1231 | 1232 | --------------------------------------------------------------------------------