I needed something to help me upload my vacation videos, so I wrote this app and now I'm sharing it with you.
23 |
I also had the bad experience of paying for a similar application, which the developers later broke, then demanded I pay for the new version to continue using. Hopefully this will help others avoid this kind of inconvenience.
No installation needed. Start the application and you'll be prompted to provide access to your YouTube account in order to upload videos.
30 |
Afterwards, you can drag videos files to the main grid of the application and edit their details. When you're finished, just hit "Upload" and the app will do its job.
31 |
To remove videos from the grid, select the lines on the grid and hit "Delete".
32 |
The title and description fields support the following placeholders: %f - file name, %i - order of file on the grid, %c - total number of files on the grid (so having five videos with title "%f - part %i/%c" will upload them with titles like "my video - part 2/5")
33 |
During upload, the application writes two files: upload-list.log, which lists the titles of the uploaded videos and their URLs, and upload.csv, which contains more details and can be opened in Excel. Copy these files if you want to keep them for reference. When uploading finishes, the application will automatically open upload-list.log.
34 |
This app should work on MacOS or Linux with Mono.
35 |
Planned Features
36 |
37 |
Reading video EXIF tags to populate title and description automatically or with a pattern supplied by the user.
38 |
39 |
Export / import lists of videos to upload in CSV file format (e.g. for compatibility with Excel).
40 |
41 |
Resuming failed uploads.
42 |
43 |
Notify user when upload is complete, e.g. via push notification or by running a shell task.
44 |
45 |
Integrate with ffmpeg for automatic stabilization, concatenation etc.
46 |
47 |
48 |
Privacy Policy
49 |
This app doesn't gather or publish ANY data from the user's machine other than to YouTube's servers for the purpose of uploading videos.
50 |
This app's use of information received from Google APIs will adhere to the Google API Services User Data Policy, including the Limited Use requirements.
To build the project you'll need to provide your own client_secret.json from Google Cloud, since if I publish the client secret used in the binary, Google will likely revoke it. Protecting the client secret is also the reason why the published release is obfuscated. Just paste the JSON file in a static class ClientSecret with a string constant.
54 |
Donations
55 |
If this tool helps you, consider sending a small donation to a charity of your choice and dropping me a line. You'll totally make my day. You can also donate a few bucks through me, in which case 100% of your donation will be forwarded to a children or animal shelter.
56 |
57 |
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # YouTube Bulk Upload UI - Lazy Edition
2 |
3 | ## IMPORTANT
4 |
5 | It seems the Google has removed the application's registration for their own inscrutable reasons. For the moment the app won't work as is and I don't have time to update it. Feel free to rebuild it with your own client-secret.json etc.
6 |
7 |
8 |
9 | A simple tool for uploading multiple videos to your YouTube channel.
10 |
11 | I needed something to help me upload my vacation videos and now I'm sharing it with you.
12 |
13 | I also had the bad experience of paying for a similar application, which the developers later broke, then demanded I pay for the new version to continue using. Hopefully this will help others avoid this kind of inconvenience.
14 |
15 | 
16 |
17 | ## How to Use
18 |
19 | Requires .NET 4.6.1. Download the latest release from here: https://github.com/staafl/youtube-bulk-upload-ui/releases/latest
20 |
21 | No installation needed. Start the application and you'll be prompted to provide access to your YouTube account in order to upload videos. The login data is stored in the %APPDATA%\YouTube.Auth.Store folder so you can delete it and run the app again to log in with another user.
22 |
23 | NB: As of 2021-07-22, Google Cloud API verification is still ongoing for the application and you'll see a "Google hasn't verified this app" screen on the consent page. You'll need to click on "Advanced" and click on the "Go to Youtube Bulk Upload UI (unsafe)" link if you want to use it. Sorry about that, but the validation process has a lot of details that need to be tweaked and a lot of back and forth over email, and I'm busy so the process is progressing slowly. You're welcome to wait a few weeks instead until verification is hopefully officially complete.
24 |
25 | After logging in, you can drag videos files to the main grid of the application and edit their details. When you're finished, just hit "Upload" and the app will do its job.
26 |
27 | To remove videos from the grid, select the lines on the grid and hit "Delete".
28 |
29 | The title and description fields support the following placeholders: %f - file name, %i - order of file on the grid, %c - total number of files on the grid (so having five videos with title "%f - part %i/%c" will upload them with titles like "my video - part 2/5")
30 |
31 | During upload, the application writes two files: upload-list.log, which lists the titles of the uploaded videos and their URLs, and upload.csv, which contains more details and can be opened in Excel. Copy these files if you want to keep them for reference. When uploading finishes, the application will automatically open upload-list.log.
32 |
33 | This app should work on MacOS or Linux with Mono.
34 |
35 | ## Planned Features
36 |
37 | - Reading video EXIF tags to populate title and description automatically or with a pattern supplied by the user.
38 |
39 | - Export / import lists of videos to upload in CSV file format (e.g. for compatibility with Excel).
40 |
41 | - Resuming failed uploads.
42 |
43 | - Notify user when upload is complete, e.g. via push notification or by running a shell task.
44 |
45 | - Integrate with ffmpeg for automatic stabilization etc.
46 |
47 | ## Privacy Policy
48 |
49 | This app doesn't gather or publish ANY data from the user's machine other than to YouTube's servers for the purpose of uploading videos.
50 |
51 | This app's use of information received from Google APIs will adhere to the [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes), including the Limited Use requirements.
52 |
53 | ## Building
54 |
55 | Download the code here: [https://github.com/staafl/youtube-bulk-upload-ui](https://github.com/staafl/youtube-bulk-upload-ui)
56 |
57 | To build the project you'll need to provide your own client_secret.json from Google Cloud, since if I publish the client secret used in the binary, Google will likely revoke it. Protecting the client secret is also the reason why the published release is obfuscated. Just paste the JSON file in a static class ClientSecret with a string constant.
58 |
59 | ## Donations
60 |
61 | If this tool helps you, consider sending a small donation to a charity of your choice and dropping me a line. You'll totally make my day. You can also donate a few bucks through me, in which case 100% of your donation will be forwarded to a children or animal shelter.
62 |
63 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=F7GH776DZEFNU)
64 |
--------------------------------------------------------------------------------
/YoutubeBulkUploadUI/YoutubeBulkUploadUI/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/YoutubeBulkUploadUI/YoutubeBulkUploadUI/YoutubeBulkUploadUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Debug
7 | AnyCPU
8 | {BA15CC42-AD77-4D9C-B857-4D0495F5067A}
9 | WinExe
10 | YoutubeBulkUploadUI
11 | YoutubeBulkUploadUI
12 | v4.6.1
13 | 512
14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 4
16 | true
17 |
18 |
19 |
20 |
21 | AnyCPU
22 | true
23 | full
24 | false
25 | bin\Debug\
26 | DEBUG;TRACE
27 | prompt
28 | 4
29 |
30 |
31 | AnyCPU
32 | pdbonly
33 | true
34 | bin\Release\
35 | TRACE
36 | prompt
37 | 4
38 |
39 |
40 |
41 | ..\packages\Google.Apis.1.51.0\lib\net45\Google.Apis.dll
42 |
43 |
44 | ..\packages\Google.Apis.Auth.1.51.0\lib\net45\Google.Apis.Auth.dll
45 |
46 |
47 | ..\packages\Google.Apis.Auth.1.51.0\lib\net45\Google.Apis.Auth.PlatformServices.dll
48 |
49 |
50 | ..\packages\Google.Apis.Core.1.51.0\lib\net45\Google.Apis.Core.dll
51 |
52 |
53 | ..\packages\Google.Apis.1.51.0\lib\net45\Google.Apis.PlatformServices.dll
54 |
55 |
56 | ..\packages\Google.Apis.YouTube.v3.1.51.0.2238\lib\net45\Google.Apis.YouTube.v3.dll
57 |
58 |
59 | ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 4.0
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | MSBuild:Compile
79 | Designer
80 |
81 |
82 |
83 |
84 | MSBuild:Compile
85 | Designer
86 |
87 |
88 | App.xaml
89 | Code
90 |
91 |
92 | MainWindow.xaml
93 | Code
94 |
95 |
96 |
97 |
98 | Code
99 |
100 |
101 | True
102 | True
103 | Resources.resx
104 |
105 |
106 | True
107 | Settings.settings
108 | True
109 |
110 |
111 | ResXFileCodeGenerator
112 | Resources.Designer.cs
113 |
114 |
115 |
116 | SettingsSingleFileGenerator
117 | Settings.Designer.cs
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | Always
126 |
127 |
128 |
129 |
130 | "$(Obfuscar)" $(ProjectDir)obfuscar.xml
131 |
132 |
133 |
134 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ClientSecret.cs
2 | client_secret*.json
3 |
4 |
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 | ##
8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
9 |
10 | # User-specific files
11 | *.rsuser
12 | *.suo
13 | *.user
14 | *.userosscache
15 | *.sln.docstates
16 |
17 | # User-specific files (MonoDevelop/Xamarin Studio)
18 | *.userprefs
19 |
20 | # Mono auto generated files
21 | mono_crash.*
22 |
23 | # Build results
24 | [Dd]ebug/
25 | [Dd]ebugPublic/
26 | [Rr]elease/
27 | [Rr]eleases/
28 | x64/
29 | x86/
30 | [Aa][Rr][Mm]/
31 | [Aa][Rr][Mm]64/
32 | bld/
33 | [Bb]in/
34 | [Oo]bj/
35 | [Ll]og/
36 | [Ll]ogs/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET Core
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.vspscc
94 | *.vssscc
95 | .builds
96 | *.pidb
97 | *.svclog
98 | *.scc
99 |
100 | # Chutzpah Test files
101 | _Chutzpah*
102 |
103 | # Visual C++ cache files
104 | ipch/
105 | *.aps
106 | *.ncb
107 | *.opendb
108 | *.opensdf
109 | *.sdf
110 | *.cachefile
111 | *.VC.db
112 | *.VC.VC.opendb
113 |
114 | # Visual Studio profiler
115 | *.psess
116 | *.vsp
117 | *.vspx
118 | *.sap
119 |
120 | # Visual Studio Trace Files
121 | *.e2e
122 |
123 | # TFS 2012 Local Workspace
124 | $tf/
125 |
126 | # Guidance Automation Toolkit
127 | *.gpState
128 |
129 | # ReSharper is a .NET coding add-in
130 | _ReSharper*/
131 | *.[Rr]e[Ss]harper
132 | *.DotSettings.user
133 |
134 | # TeamCity is a build add-in
135 | _TeamCity*
136 |
137 | # DotCover is a Code Coverage Tool
138 | *.dotCover
139 |
140 | # AxoCover is a Code Coverage Tool
141 | .axoCover/*
142 | !.axoCover/settings.json
143 |
144 | # Visual Studio code coverage results
145 | *.coverage
146 | *.coveragexml
147 |
148 | # NCrunch
149 | _NCrunch_*
150 | .*crunch*.local.xml
151 | nCrunchTemp_*
152 |
153 | # MightyMoose
154 | *.mm.*
155 | AutoTest.Net/
156 |
157 | # Web workbench (sass)
158 | .sass-cache/
159 |
160 | # Installshield output folder
161 | [Ee]xpress/
162 |
163 | # DocProject is a documentation generator add-in
164 | DocProject/buildhelp/
165 | DocProject/Help/*.HxT
166 | DocProject/Help/*.HxC
167 | DocProject/Help/*.hhc
168 | DocProject/Help/*.hhk
169 | DocProject/Help/*.hhp
170 | DocProject/Help/Html2
171 | DocProject/Help/html
172 |
173 | # Click-Once directory
174 | publish/
175 |
176 | # Publish Web Output
177 | *.[Pp]ublish.xml
178 | *.azurePubxml
179 | # Note: Comment the next line if you want to checkin your web deploy settings,
180 | # but database connection strings (with potential passwords) will be unencrypted
181 | *.pubxml
182 | *.publishproj
183 |
184 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
185 | # checkin your Azure Web App publish settings, but sensitive information contained
186 | # in these scripts will be unencrypted
187 | PublishScripts/
188 |
189 | # NuGet Packages
190 | *.nupkg
191 | # NuGet Symbol Packages
192 | *.snupkg
193 | # The packages folder can be ignored because of Package Restore
194 | **/[Pp]ackages/*
195 | # except build/, which is used as an MSBuild target.
196 | !**/[Pp]ackages/build/
197 | # Uncomment if necessary however generally it will be regenerated when needed
198 | #!**/[Pp]ackages/repositories.config
199 | # NuGet v3's project.json files produces more ignorable files
200 | *.nuget.props
201 | *.nuget.targets
202 |
203 | # Microsoft Azure Build Output
204 | csx/
205 | *.build.csdef
206 |
207 | # Microsoft Azure Emulator
208 | ecf/
209 | rcf/
210 |
211 | # Windows Store app package directories and files
212 | AppPackages/
213 | BundleArtifacts/
214 | Package.StoreAssociation.xml
215 | _pkginfo.txt
216 | *.appx
217 | *.appxbundle
218 | *.appxupload
219 |
220 | # Visual Studio cache files
221 | # files ending in .cache can be ignored
222 | *.[Cc]ache
223 | # but keep track of directories ending in .cache
224 | !?*.[Cc]ache/
225 |
226 | # Others
227 | ClientBin/
228 | ~$*
229 | *~
230 | *.dbmdl
231 | *.dbproj.schemaview
232 | *.jfm
233 | *.pfx
234 | *.publishsettings
235 | orleans.codegen.cs
236 |
237 | # Including strong name files can present a security risk
238 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
239 | #*.snk
240 |
241 | # Since there are multiple workflows, uncomment next line to ignore bower_components
242 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
243 | #bower_components/
244 |
245 | # RIA/Silverlight projects
246 | Generated_Code/
247 |
248 | # Backup & report files from converting an old project file
249 | # to a newer Visual Studio version. Backup files are not needed,
250 | # because we have git ;-)
251 | _UpgradeReport_Files/
252 | Backup*/
253 | UpgradeLog*.XML
254 | UpgradeLog*.htm
255 | ServiceFabricBackup/
256 | *.rptproj.bak
257 |
258 | # SQL Server files
259 | *.mdf
260 | *.ldf
261 | *.ndf
262 |
263 | # Business Intelligence projects
264 | *.rdl.data
265 | *.bim.layout
266 | *.bim_*.settings
267 | *.rptproj.rsuser
268 | *- [Bb]ackup.rdl
269 | *- [Bb]ackup ([0-9]).rdl
270 | *- [Bb]ackup ([0-9][0-9]).rdl
271 |
272 | # Microsoft Fakes
273 | FakesAssemblies/
274 |
275 | # GhostDoc plugin setting file
276 | *.GhostDoc.xml
277 |
278 | # Node.js Tools for Visual Studio
279 | .ntvs_analysis.dat
280 | node_modules/
281 |
282 | # Visual Studio 6 build log
283 | *.plg
284 |
285 | # Visual Studio 6 workspace options file
286 | *.opt
287 |
288 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
289 | *.vbw
290 |
291 | # Visual Studio LightSwitch build output
292 | **/*.HTMLClient/GeneratedArtifacts
293 | **/*.DesktopClient/GeneratedArtifacts
294 | **/*.DesktopClient/ModelManifest.xml
295 | **/*.Server/GeneratedArtifacts
296 | **/*.Server/ModelManifest.xml
297 | _Pvt_Extensions
298 |
299 | # Paket dependency manager
300 | .paket/paket.exe
301 | paket-files/
302 |
303 | # FAKE - F# Make
304 | .fake/
305 |
306 | # CodeRush personal settings
307 | .cr/personal
308 |
309 | # Python Tools for Visual Studio (PTVS)
310 | __pycache__/
311 | *.pyc
312 |
313 | # Cake - Uncomment if you are using it
314 | # tools/**
315 | # !tools/packages.config
316 |
317 | # Tabs Studio
318 | *.tss
319 |
320 | # Telerik's JustMock configuration file
321 | *.jmconfig
322 |
323 | # BizTalk build output
324 | *.btp.cs
325 | *.btm.cs
326 | *.odx.cs
327 | *.xsd.cs
328 |
329 | # OpenCover UI analysis results
330 | OpenCover/
331 |
332 | # Azure Stream Analytics local run output
333 | ASALocalRun/
334 |
335 | # MSBuild Binary and Structured Log
336 | *.binlog
337 |
338 | # NVidia Nsight GPU debugger configuration file
339 | *.nvuser
340 |
341 | # MFractors (Xamarin productivity tool) working folder
342 | .mfractor/
343 |
344 | # Local History for Visual Studio
345 | .localhistory/
346 |
347 | # BeatPulse healthcheck temp database
348 | healthchecksdb
349 |
350 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
351 | MigrationBackup/
352 |
353 | # Ionide (cross platform F# VS Code tools) working folder
354 | .ionide/
355 |
--------------------------------------------------------------------------------
/categories.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "youtube#videoCategoryListResponse",
3 | "etag": "QteLrrS_X7rM7rlcU_e7qa0embQ",
4 | "items": [
5 | {
6 | "kind": "youtube#videoCategory",
7 | "etag": "grPOPYEUUZN3ltuDUGEWlrTR90U",
8 | "id": "1",
9 | "snippet": {
10 | "title": "Film & Animation",
11 | "assignable": true,
12 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
13 | }
14 | },
15 | {
16 | "kind": "youtube#videoCategory",
17 | "etag": "Q0xgUf8BFM8rW3W0R9wNq809xyA",
18 | "id": "2",
19 | "snippet": {
20 | "title": "Autos & Vehicles",
21 | "assignable": true,
22 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
23 | }
24 | },
25 | {
26 | "kind": "youtube#videoCategory",
27 | "etag": "qnpwjh5QlWM5hrnZCvHisquztC4",
28 | "id": "10",
29 | "snippet": {
30 | "title": "Music",
31 | "assignable": true,
32 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
33 | }
34 | },
35 | {
36 | "kind": "youtube#videoCategory",
37 | "etag": "HyFIixS5BZaoBdkQdLzPdoXWipg",
38 | "id": "15",
39 | "snippet": {
40 | "title": "Pets & Animals",
41 | "assignable": true,
42 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
43 | }
44 | },
45 | {
46 | "kind": "youtube#videoCategory",
47 | "etag": "PNU8SwXhjsF90fmkilVohofOi4I",
48 | "id": "17",
49 | "snippet": {
50 | "title": "Sports",
51 | "assignable": true,
52 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
53 | }
54 | },
55 | {
56 | "kind": "youtube#videoCategory",
57 | "etag": "5kFljz9YJ4lEgSfVwHWi5kTAwAs",
58 | "id": "18",
59 | "snippet": {
60 | "title": "Short Movies",
61 | "assignable": false,
62 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
63 | }
64 | },
65 | {
66 | "kind": "youtube#videoCategory",
67 | "etag": "ANnLQyzEA_9m3bMyJXMhKTCOiyg",
68 | "id": "19",
69 | "snippet": {
70 | "title": "Travel & Events",
71 | "assignable": true,
72 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
73 | }
74 | },
75 | {
76 | "kind": "youtube#videoCategory",
77 | "etag": "0Hh6gbZ9zWjnV3sfdZjKB5LQr6E",
78 | "id": "20",
79 | "snippet": {
80 | "title": "Gaming",
81 | "assignable": true,
82 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
83 | }
84 | },
85 | {
86 | "kind": "youtube#videoCategory",
87 | "etag": "q8Cp4pUfCD8Fuh8VJ_yl5cBCVNw",
88 | "id": "21",
89 | "snippet": {
90 | "title": "Videoblogging",
91 | "assignable": false,
92 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
93 | }
94 | },
95 | {
96 | "kind": "youtube#videoCategory",
97 | "etag": "cHDaaqPDZsJT1FPr1-MwtyIhR28",
98 | "id": "22",
99 | "snippet": {
100 | "title": "People & Blogs",
101 | "assignable": true,
102 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
103 | }
104 | },
105 | {
106 | "kind": "youtube#videoCategory",
107 | "etag": "3Uz364xBbKY50a2s0XQlv-gXJds",
108 | "id": "23",
109 | "snippet": {
110 | "title": "Comedy",
111 | "assignable": true,
112 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
113 | }
114 | },
115 | {
116 | "kind": "youtube#videoCategory",
117 | "etag": "0srcLUqQzO7-NGLF7QnhdVzJQmY",
118 | "id": "24",
119 | "snippet": {
120 | "title": "Entertainment",
121 | "assignable": true,
122 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
123 | }
124 | },
125 | {
126 | "kind": "youtube#videoCategory",
127 | "etag": "bQlQMjmYX7DyFkX4w3kT0osJyIc",
128 | "id": "25",
129 | "snippet": {
130 | "title": "News & Politics",
131 | "assignable": true,
132 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
133 | }
134 | },
135 | {
136 | "kind": "youtube#videoCategory",
137 | "etag": "Y06N41HP_WlZmeREZvkGF0HW5pg",
138 | "id": "26",
139 | "snippet": {
140 | "title": "Howto & Style",
141 | "assignable": true,
142 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
143 | }
144 | },
145 | {
146 | "kind": "youtube#videoCategory",
147 | "etag": "yBaNkLx4sX9NcDmFgAmxQcV4Y30",
148 | "id": "27",
149 | "snippet": {
150 | "title": "Education",
151 | "assignable": true,
152 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
153 | }
154 | },
155 | {
156 | "kind": "youtube#videoCategory",
157 | "etag": "Mxy3A-SkmnR7MhJDZRS4DuAIbQA",
158 | "id": "28",
159 | "snippet": {
160 | "title": "Science & Technology",
161 | "assignable": true,
162 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
163 | }
164 | },
165 | {
166 | "kind": "youtube#videoCategory",
167 | "etag": "p3lEirEJApyEkuWpaGEHoF-m-aA",
168 | "id": "29",
169 | "snippet": {
170 | "title": "Nonprofits & Activism",
171 | "assignable": true,
172 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
173 | }
174 | },
175 | {
176 | "kind": "youtube#videoCategory",
177 | "etag": "4pIHL_AdN2kO7btAGAP1TvPucNk",
178 | "id": "30",
179 | "snippet": {
180 | "title": "Movies",
181 | "assignable": false,
182 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
183 | }
184 | },
185 | {
186 | "kind": "youtube#videoCategory",
187 | "etag": "Iqol1myDwh2AuOnxjtn2AfYwJTU",
188 | "id": "31",
189 | "snippet": {
190 | "title": "Anime/Animation",
191 | "assignable": false,
192 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
193 | }
194 | },
195 | {
196 | "kind": "youtube#videoCategory",
197 | "etag": "tzhBKCBcYWZLPai5INY4id91ss8",
198 | "id": "32",
199 | "snippet": {
200 | "title": "Action/Adventure",
201 | "assignable": false,
202 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
203 | }
204 | },
205 | {
206 | "kind": "youtube#videoCategory",
207 | "etag": "ii8nBGYpKyl6FyzP3cmBCevdrbs",
208 | "id": "33",
209 | "snippet": {
210 | "title": "Classics",
211 | "assignable": false,
212 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
213 | }
214 | },
215 | {
216 | "kind": "youtube#videoCategory",
217 | "etag": "Y0u9UAQCCGp60G11Arac5Mp46z4",
218 | "id": "34",
219 | "snippet": {
220 | "title": "Comedy",
221 | "assignable": false,
222 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
223 | }
224 | },
225 | {
226 | "kind": "youtube#videoCategory",
227 | "etag": "_YDnyT205AMuX8etu8loOiQjbD4",
228 | "id": "35",
229 | "snippet": {
230 | "title": "Documentary",
231 | "assignable": false,
232 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
233 | }
234 | },
235 | {
236 | "kind": "youtube#videoCategory",
237 | "etag": "eAl2b-uqIGRDgnlMa0EsGZjXmWg",
238 | "id": "36",
239 | "snippet": {
240 | "title": "Drama",
241 | "assignable": false,
242 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
243 | }
244 | },
245 | {
246 | "kind": "youtube#videoCategory",
247 | "etag": "HDAW2HFOt3SqeDI00X-eL7OELfY",
248 | "id": "37",
249 | "snippet": {
250 | "title": "Family",
251 | "assignable": false,
252 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
253 | }
254 | },
255 | {
256 | "kind": "youtube#videoCategory",
257 | "etag": "QHiWh3niw5hjDrim85M8IGF45eE",
258 | "id": "38",
259 | "snippet": {
260 | "title": "Foreign",
261 | "assignable": false,
262 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
263 | }
264 | },
265 | {
266 | "kind": "youtube#videoCategory",
267 | "etag": "ztKcSS7GpH9uEyZk9nQCdNujvGg",
268 | "id": "39",
269 | "snippet": {
270 | "title": "Horror",
271 | "assignable": false,
272 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
273 | }
274 | },
275 | {
276 | "kind": "youtube#videoCategory",
277 | "etag": "Ids1sm8QFeSo_cDlpcUNrnEBYWA",
278 | "id": "40",
279 | "snippet": {
280 | "title": "Sci-Fi/Fantasy",
281 | "assignable": false,
282 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
283 | }
284 | },
285 | {
286 | "kind": "youtube#videoCategory",
287 | "etag": "qhfgS7MzzZHIy_UZ1dlawl1GbnY",
288 | "id": "41",
289 | "snippet": {
290 | "title": "Thriller",
291 | "assignable": false,
292 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
293 | }
294 | },
295 | {
296 | "kind": "youtube#videoCategory",
297 | "etag": "TxVSfGoUyT7CJ7h7ebjg4vhIt6g",
298 | "id": "42",
299 | "snippet": {
300 | "title": "Shorts",
301 | "assignable": false,
302 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
303 | }
304 | },
305 | {
306 | "kind": "youtube#videoCategory",
307 | "etag": "o9w6eNqzjHPnNbKDujnQd8pklXM",
308 | "id": "43",
309 | "snippet": {
310 | "title": "Shows",
311 | "assignable": false,
312 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
313 | }
314 | },
315 | {
316 | "kind": "youtube#videoCategory",
317 | "etag": "mLdyKd0VgXKDI6GevTLBAcvRlIU",
318 | "id": "44",
319 | "snippet": {
320 | "title": "Trailers",
321 | "assignable": false,
322 | "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
323 | }
324 | }
325 | ]
326 | }
327 |
--------------------------------------------------------------------------------
/YoutubeBulkUploadUI/YoutubeBulkUploadUI/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using Google.Apis.Auth.OAuth2;
2 | using Google.Apis.Services;
3 | using Google.Apis.Util.Store;
4 | using Google.Apis.YouTube.v3;
5 | using Google.Apis.YouTube.v3.Data;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Collections.ObjectModel;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Reflection;
12 | using System.Text;
13 | using System.Threading;
14 | using System.Windows;
15 | using System.Windows.Input;
16 | using Google.Apis.Upload;
17 | using System.ComponentModel;
18 | using System.Runtime.InteropServices;
19 | using System.Diagnostics;
20 | using Google;
21 |
22 | namespace YoutubeBulkUploadUI
23 | {
24 | ///
25 | /// Interaction logic for MainWindow.xaml
26 | ///
27 | public partial class MainWindow : Window
28 | {
29 | readonly ObservableCollection filesCollection = new ObservableCollection();
30 | readonly ObservableCollection categoriesCollection = new ObservableCollection();
31 | FileStream fileStream;
32 | FileModel currentUpload;
33 | YouTubeService youtubeService;
34 | string categories = @"1,Film & Animation
35 | 2,Autos & Vehicles
36 | 10,Music
37 | 15,Pets & Animals
38 | 17,Sports
39 | 18,Short Movies
40 | 19,Travel & Events
41 | 20,Gaming
42 | 21,Videoblogging
43 | 22,People & Blogs
44 | 23,Comedy
45 | 24,Entertainment
46 | 25,News & Politics
47 | 26,Howto & Style
48 | 27,Education
49 | 28,Science & Technology
50 | 30,Movies
51 | 31,Anime/Animation
52 | 32,Action/Adventure
53 | 33,Classics
54 | 34,Comedy
55 | 35,Documentary
56 | 36,Drama
57 | 37,Family
58 | 38,Foreign
59 | 39,Horror
60 | 40,Sci-Fi/Fantasy
61 | 41,Thriller
62 | 42,Shorts
63 | 43,Shows
64 | 44,Trailers
65 | ";
66 |
67 | class CategoryModel
68 | {
69 | public string Id { get; set; }
70 | public string Name { get; set; }
71 | }
72 | class FileModel : INotifyPropertyChanged
73 | {
74 | string status;
75 | string url;
76 | public string File { get; set; }
77 | public string Status { get { return status; } set { status = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (nameof(Status))); } }
78 | public string Url { get { return url; } set { url = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (nameof(Url))); } }
79 | public string Length { get; set; }
80 | public string Title { get; set; }
81 | // todo: get from EXIF tags
82 | public string Description { get; set; }
83 | public VideoVisibility Visibility { get; set; }
84 | public Video Video { get; set; }
85 | public bool MadeForKids { get; set; }
86 | public CategoryModel Category { get; set; }
87 | public string Tags { get; set; }
88 |
89 | public event PropertyChangedEventHandler PropertyChanged;
90 | }
91 |
92 | public MainWindow()
93 | {
94 | InitializeComponent();
95 | this.DataContext = new
96 | {
97 | Files = filesCollection,
98 | Categories = categoriesCollection
99 | };
100 | dgv.AllowDrop = true;
101 | dgv.Drop += Dgv_Drop;
102 |
103 | }
104 |
105 | protected override void OnKeyUp(KeyEventArgs e)
106 | {
107 | if (e.Key == Key.F1)
108 | {
109 | Process.Start("http://trustingwolves.com/youtube-bulk-upload/index.html#how-to-use");
110 | }
111 | base.OnKeyUp(e);
112 | }
113 |
114 | protected async override void OnContentRendered(EventArgs e)
115 | {
116 | base.OnContentRendered(e);
117 |
118 | UserCredential credential;
119 | using (var stream =
120 | (ClientSecret.clientSecret != null ? new MemoryStream(Encoding.UTF8.GetBytes(ClientSecret.clientSecret)) : null) ??
121 | Assembly.GetExecutingAssembly().GetManifestResourceStream("YoutubeBulkUploadUI.client_secret.json")
122 | ?? (Stream)new FileStream("client_secret.json", FileMode.Open, FileAccess.Read))
123 | {
124 | credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
125 | GoogleClientSecrets.Load(stream).Secrets,
126 | new[] { YouTubeService.Scope.Youtube, YouTubeService.Scope.YoutubeUpload },
127 | "user",
128 | CancellationToken.None,
129 | new FileDataStore("YouTube.Auth.Store")).Result;
130 | }
131 |
132 | youtubeService = new YouTubeService(
133 | new BaseClientService.Initializer()
134 | {
135 | HttpClientInitializer = credential,
136 | ApplicationName = Assembly.GetExecutingAssembly().GetName().Name
137 | });
138 |
139 | // https://developers.google.com/youtube/v3/docs/videoCategories/list?apix_params=%7B%22part%22%3A%5B%22snippet%22%5D%2C%22regionCode%22%3A%22us%22%7D
140 | categoriesCollection.Add(null);
141 | //if (File.Exists("categories.txt"))
142 | {
143 | foreach (var line in categories.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) //File.ReadAllLines("categories.txt"))
144 | {
145 | var split = line.Split(new[] { ',' }, 2);
146 | categoriesCollection.Add(
147 | new CategoryModel
148 | {
149 | Id = split[0],
150 | Name = split[1]
151 | });
152 | }
153 | }
154 | /*else
155 | {
156 | string regionName;
157 | try
158 | {
159 | string info = new WebClient().DownloadString("http://ipinfo.io");
160 | regionName = Regex.Match(info, "\"country\": *\"([^\"]+)\"").Groups[1].Value;
161 | }
162 | catch
163 | {
164 | regionName = "us";
165 | }
166 |
167 | //var ipInfo = jsonObject.Deserialize(info);
168 |
169 | RegionInfo region = new RegionInfo(regionName);
170 |
171 |
172 | var categoriesRequest = youtubeService
173 | .VideoCategories
174 | .List("snippet");
175 | categoriesRequest.RegionCode = region.Name.ToLower();
176 |
177 | var categories = await categoriesRequest.ExecuteAsync();
178 | foreach (var category in categories.Items)
179 | {
180 | categoriesCollection.Add(
181 | new CategoryModel
182 | {
183 | Id = category.Id,
184 | Name = category.Snippet.Title
185 | });
186 | File.AppendAllLines("categories.txt", new[] { category.Id + "," + category.Snippet.Title });
187 | }
188 | }*/
189 |
190 | }
191 |
192 | public class IpInfo
193 | {
194 | //country
195 | public string Country { get; set; }
196 | }
197 |
198 | private void videosInsertRequest_ResponseReceived(Video obj)
199 | {
200 | var file = filesCollection.FirstOrDefault(x => x.Title == obj.Snippet.Title);
201 | string error =
202 | obj.ProcessingDetails?.ProcessingFailureReason ??
203 | obj.Status?.RejectionReason ??
204 | obj.Status.FailureReason;
205 | if (error != null)
206 | {
207 | file.Status = "Error";
208 | file.Url = error;
209 | }
210 | else if (obj.Status?.UploadStatus != null)
211 | {
212 | file.Status = obj.Status.UploadStatus;
213 | if (obj.Id != null)
214 | {
215 | file.Url = "https://youtube.com/watch?v=" + obj.Id;
216 | }
217 | }
218 | }
219 |
220 | private void videosInsertRequest_ProgressChanged(IUploadProgress obj)
221 | {
222 | this.Dispatcher.BeginInvoke(new Action(() =>
223 | {
224 | try
225 | {
226 | progress.Value = (int)(100 * (obj.BytesSent / (double)fileStream.Length));
227 | }
228 | catch (ObjectDisposedException)
229 | {
230 | progress.Value = 100;
231 | }
232 | }));
233 | }
234 |
235 | [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
236 | private static extern int StrCmpLogicalW(string psz1, string psz2);
237 | class StrCmpLogicalWComparer : IComparer
238 | {
239 | public int Compare(string x, string y)
240 | {
241 | return StrCmpLogicalW(x, y);
242 | }
243 | }
244 |
245 | private void Dgv_Drop(object sender, DragEventArgs e)
246 | {
247 | // Shell IDList Array;DragImageBits;DragContext;DragSourceHelperFlags;InShellDragLoop;FileDrop;FileNameW;FileName
248 |
249 | if (e.Data.GetDataPresent("FileDrop"))
250 | {
251 | var files = (e.Data.GetData("FileDrop") as string[]);
252 | Array.Sort(files, new StrCmpLogicalWComparer());
253 |
254 | foreach (var file in files)
255 | {
256 | var model = new FileModel
257 | {
258 | File = file,
259 | Status = "Pending",
260 | Visibility = VideoVisibility.Unlisted,
261 | Title = "%f", //System.IO.Path.GetFileNameWithoutExtension(file),
262 | Category = categoriesCollection.First(),
263 | Description = ""
264 | };
265 | if (copy.IsChecked == true && filesCollection.Any())
266 | {
267 | var last = filesCollection.Last();
268 | model.Visibility = last.Visibility;
269 | model.Title = last.Title;
270 | model.Description = last.Description;
271 | model.Category = last.Category;
272 | model.MadeForKids = last.MadeForKids;
273 | model.Tags = last.Tags;
274 | }
275 | filesCollection.Add(model);
276 | }
277 | }
278 | }
279 |
280 | private async void but_upload_Click(object sender, RoutedEventArgs e)
281 | {
282 | but_upload.IsEnabled = false;
283 | int ii = 0;
284 | foreach (var file in filesCollection)
285 | {
286 | currentUpload = file;
287 | file.Status = "Uploading...";
288 | ii += 1;
289 | label.Content = "Uploading video " + ii + ": " + System.IO.Path.GetFileName(file.File);
290 | var filePath = file.File;
291 | var video = new Video();
292 | video.Snippet = new VideoSnippet();
293 | video.Snippet.Title = file.Title = ReplacePatterns(file, file.Title, ii);
294 | video.Snippet.Description = file.Description = ReplacePatterns(file, file.Description, ii);
295 | video.Snippet.Tags = file.Tags?.Split(new[] { ' ', ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
296 | video.Snippet.CategoryId = file.Category?.Id; // See https://developers.google.com/youtube/v3/docs/videoCategories/list
297 | video.Status = new VideoStatus();
298 | video.Status.PrivacyStatus = (file.Visibility + "").ToLower();
299 | video.Status.MadeForKids = file.MadeForKids;
300 | file.Video = video;
301 | using (fileStream = new FileStream(filePath, FileMode.Open))
302 | {
303 | var videosInsertRequest = youtubeService.Videos.Insert(video, "snippet,status", fileStream, "video/*");
304 | videosInsertRequest.ProgressChanged += videosInsertRequest_ProgressChanged;
305 | videosInsertRequest.ResponseReceived += videosInsertRequest_ResponseReceived;
306 | var uploadProgress = await videosInsertRequest.UploadAsync();
307 | file.Status = uploadProgress.Status.ToString();
308 | if (uploadProgress.Exception != null)
309 | {
310 | file.Url = (uploadProgress.Exception as GoogleApiException)?.Error?.Message ?? uploadProgress.Exception.Message;
311 | }
312 | //file.Status = "Uploaded";
313 | // TODO: are we finished at this point?
314 | progress.Value = 100;
315 | }
316 | }
317 | label.Content = "Done!";
318 |
319 | if (!File.Exists("upload.csv"))
320 | {
321 | File.WriteAllText("upload.csv", "File,Status,Result,Title,Description,Category,Tags,Made for Kids\n");
322 | }
323 | using (var sw = new StreamWriter("upload.csv", append: true))
324 | {
325 | foreach (var file in filesCollection)
326 | {
327 | var csvLine = new[]{
328 | file.File,
329 | file.Status,
330 | file.Url,
331 | file.Title,
332 | file.Description,
333 | file.Category?.Name,
334 | string.Join(", ", file.Tags),
335 | file.MadeForKids + ""
336 | }.Select(x => (x + "").Replace("\"", "\"\"").Replace("\r", "").Replace("\n", " "));
337 | sw.WriteLine("\"" + string.Join("\",\"", csvLine) + "\"");
338 | }
339 | }
340 | using (var sw = new StreamWriter("upload-list.log"))
341 | {
342 | foreach (var file in filesCollection)
343 | {
344 | sw.WriteLine(file.Title + ": " + file.Url);
345 | }
346 | }
347 | Process.Start("notepad.exe", "upload-list.log");
348 | but_upload.IsEnabled = true;
349 | }
350 |
351 | private string ReplacePatterns(FileModel model, string what, int index)
352 | {
353 | return what
354 | .Replace("%i", index + "")
355 | .Replace("%c", filesCollection.Count + "")
356 | .Replace("%f", System.IO.Path.GetFileNameWithoutExtension(model.File));
357 | }
358 | }
359 | }
360 |
--------------------------------------------------------------------------------