├── .deployment ├── .gitignore ├── LICENSE ├── README.md ├── VideoTaggingTool.njsproj ├── VideoTaggingTool.sln ├── auth └── passport.js ├── azuredeploy.json ├── config ├── index.js └── sample.config.private.json ├── deploy.cmd ├── deploy ├── azure.md ├── img │ ├── img10.png │ ├── img100.png │ ├── img110.png │ ├── img115.png │ ├── img120.png │ ├── img130.png │ ├── img140.png │ ├── img150.png │ ├── img160.png │ ├── img170.png │ ├── img180.png │ ├── img20.png │ ├── img30.png │ ├── img40.png │ ├── img50.png │ ├── img60.png │ ├── img70.png │ ├── img80.png │ └── img90.png └── local.md ├── package.json ├── public ├── bower.json ├── css │ └── style.css ├── favicon.ico ├── images │ ├── ajax-loader.gif │ ├── logo-text.png │ └── logo.png ├── index.html ├── js │ ├── app.js │ └── controllers.js └── partials │ ├── about.html │ ├── contact.html │ ├── jobs.html │ ├── tagJob.html │ ├── terms.html │ ├── upsertUser.html │ ├── upsertVideo.html │ ├── upsertjob.html │ ├── users.html │ ├── videos.html │ └── welcome.html ├── routes ├── api.js └── login.js ├── server.js └── storage ├── blob.js ├── db.js └── sql ├── createdb.sql ├── ensureSchema.js └── schema.sql /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | command = deploy.cmd -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | public/bower_components 29 | 30 | # secrets 31 | *.private.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Catalyst Code 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Tagging App 2 | A single-page, angular.js based web application that provides a basic holistic solution for managing users, videos and tagging jobs. It uses the [Video-Tagging HTML control](https://github.com/CatalystCode/Video-Tagging) to demonstrate a real use of it in an actual web app. 3 | The tool comes with built-in authentication and authorization mechanisms. We used google for the authentication, and defined two main roles for the authorization. Each user is either an Admin or an Editor. 4 | 5 | An Admin is able to access all of the areas in the app, to add users, upload videos and create video-tagging jobs (assigning users to videos). An Admin can also review and approve video-tagging jobs, as well as fix specific tags while reviewing. 6 | An Editor can only view his jobs list, and do the actual tagging work. When the tagging-work is done, the editor sends the job for review, which is done by an Admin that reviews and approves it. 7 | In the end, the tags can also be downloaded (json format) to be used with the video-processing algorithms. 8 | 9 | The data that we save for each entity (user, video, job) was designed to be extensible. Users can use the tool as is, with its current DB schema (Sql server), and add more data items without changing the schema. In the tool, for example, we keep various of metadata items for a job, like RegionType for example, to define if we would like to tag a specific location, or an area in the frames. 10 | 11 | The server side code isn't aware of this data. It is just being used as a pipe between the client side and the storage layer. It was important for us to provide a framework that will enable adding features without changing the schema, or at least minimizing the amount of changes required to add more feature to the tool. 12 | 13 | ## Features 14 | * **Google authentication** 15 | * **User management**- add/modify users 16 | * **Roles**- users can be either Admins or Editors 17 | * **Authorization**- users only see content based on their role. Also- authorization is being enforced on the server side 18 | * **Uploading videos + labeling**- manage videos, upload/modify and label them. Sort videos by labels 19 | * **Jobs**- creating tagging jobs for users, assigning videos to users 20 | * **Video Tagging**- using the [Video Tagging](https://github.com/CatalystCode/video-tagging) control to tag videos frame by frame 21 | 22 | ## Deploying the app 23 | * Follow [these](deploy/azure.md) instructions to deploy the app on Azure. 24 | * Follow [these](deploy/local.md) instructions to run the app locally. 25 | 26 | 27 | 28 | 29 | 30 | # License 31 | [MIT](LICENSE) 32 | -------------------------------------------------------------------------------- /VideoTaggingTool.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.23107.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "VideoTaggingTool", "VideoTaggingTool.njsproj", "{7DE89CBD-49C7-4D8F-AC32-18C7172B1C9D}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {7DE89CBD-49C7-4D8F-AC32-18C7172B1C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {7DE89CBD-49C7-4D8F-AC32-18C7172B1C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {7DE89CBD-49C7-4D8F-AC32-18C7172B1C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {7DE89CBD-49C7-4D8F-AC32-18C7172B1C9D}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /auth/passport.js: -------------------------------------------------------------------------------- 1 | var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 2 | 3 | var config = require('../config').auth.google; 4 | var db = require('../storage/db'); 5 | 6 | function findUser(email, cb) { 7 | db.getUserByEmail(email, cb); 8 | } 9 | 10 | module.exports = function(passport) { 11 | 12 | // used to serialize the user for the session 13 | passport.serializeUser(function(userProfile, cb) { 14 | return cb(null, userProfile); 15 | }); 16 | 17 | // used to deserialize the user 18 | passport.deserializeUser(function (userProfile, cb) { 19 | return cb(null, userProfile); 20 | }); 21 | 22 | passport.use('google', new GoogleStrategy(config, 23 | function(token, refreshToken, profile, cb) { 24 | 25 | // make the code asynchronous 26 | // User.findOne won't fire until we have all our data back from Google 27 | process.nextTick(function() { 28 | 29 | var userProfile = { 30 | name: profile.displayName, 31 | email: profile.emails[0].value, 32 | image: profile.photos.length && profile.photos[0] && profile.photos[0].value 33 | }; 34 | 35 | // try to find the user based on their google id 36 | findUser(userProfile.email, function(err, user) { 37 | if (err) return cb(err); 38 | 39 | if (user) { 40 | user.Authorized = true; 41 | user.ImageUrl = userProfile.image; 42 | return cb(null, user); 43 | } 44 | else { 45 | return cb(null, { 46 | Name: userProfile.name, 47 | Email: userProfile.email, 48 | ImageUrl: userProfile.image, 49 | Authorized: false 50 | }); 51 | } 52 | }); 53 | }); 54 | })); 55 | }; 56 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 4 | "contentVersion": "1.0.0.0", 5 | "parameters": { 6 | "Admin_Username": { 7 | "type": "string", 8 | "defaultValue": "VideoTaggingAdmin", 9 | "metadata": { 10 | "description": "The admin user of the SQL Server and Database" 11 | } 12 | }, 13 | "Admin_Email":{ 14 | "type": "string", 15 | "metadata": { 16 | "description": "The admin email for the Database." 17 | } 18 | }, 19 | "Admin_Password": { 20 | "type": "securestring", 21 | "metadata": { 22 | "description": "The password of the admin user of the SQL Server and and Database" 23 | } 24 | }, 25 | "App_Tier": { 26 | "type": "string", 27 | "defaultValue": "S1", 28 | "allowedValues": [ 29 | "F1", 30 | "D1", 31 | "B1", 32 | "B2", 33 | "B3", 34 | "S1", 35 | "S2", 36 | "S3", 37 | "P1", 38 | "P2", 39 | "P3", 40 | "P4" 41 | ], 42 | "metadata": { 43 | "description": "Describes plan's pricing tier and instance size. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/" 44 | } 45 | }, 46 | "App_Capacity": { 47 | "type": "int", 48 | "defaultValue": 1, 49 | "minValue": 1, 50 | "metadata": { 51 | "description": "Describes plan's instance count" 52 | } 53 | }, 54 | "Storage_Account_Tier": { 55 | "type": "string", 56 | "defaultValue": "Standard_LRS", 57 | "allowedValues": [ 58 | "Standard_LRS", 59 | "Standard_GRS", 60 | "Standard_ZRS", 61 | "Premium_LRS" 62 | ], 63 | "metadata": { 64 | "description": "Storage Account type" 65 | } 66 | }, 67 | "GOOGLE_CLIENT_ID":{ 68 | "type": "string", 69 | "metadata": { 70 | "description": "The client id for google authentication." 71 | } 72 | }, 73 | "GOOGLE_CLIENT_SECRET":{ 74 | "type": "string", 75 | "metadata": { 76 | "description": "The client secret for google authentication." 77 | } 78 | } 79 | }, 80 | "variables": { 81 | "hostingPlanName": "[concat('hostingplan', uniqueString(resourceGroup().id))]", 82 | "webSiteName": "[concat('webSite', uniqueString(resourceGroup().id))]", 83 | "repoURL": "https://github.com/aribornstein/VideoTaggingTool.git", 84 | "branch": "master", 85 | "sqlserverName": "[concat('sqlserver', uniqueString(resourceGroup().id))]", 86 | "databaseName": "video_tagging_db", 87 | "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'standardsa')]" 88 | }, 89 | "resources": [ 90 | { 91 | "name": "[variables('sqlserverName')]", 92 | "type": "Microsoft.Sql/servers", 93 | "location": "[resourceGroup().location]", 94 | "tags": { 95 | "displayName": "SqlServer" 96 | }, 97 | "apiVersion": "2014-04-01", 98 | "properties": { 99 | "administratorLogin": "[parameters('Admin_Username')]", 100 | "administratorLoginPassword": "[parameters('Admin_Password')]", 101 | "version": "12.0" 102 | }, 103 | "resources": [ 104 | { 105 | "name": "[variables('databaseName')]", 106 | "type": "databases", 107 | "location": "[resourceGroup().location]", 108 | "tags": { 109 | "displayName": "Database" 110 | }, 111 | "apiVersion": "2015-01-01", 112 | "dependsOn": [ 113 | "[variables('sqlserverName')]" 114 | ], 115 | "properties": { 116 | "edition": "Basic", 117 | "collation": "SQL_Latin1_General_CP1_CI_AS", 118 | "maxSizeBytes": "1073741824", 119 | "requestedServiceObjectiveName": "Basic" 120 | } 121 | }, 122 | { 123 | "type": "firewallrules", 124 | "apiVersion": "2014-04-01", 125 | "dependsOn": [ 126 | "[variables('sqlserverName')]" 127 | ], 128 | "location": "[resourceGroup().location]", 129 | "name": "AllowAllWindowsAzureIps", 130 | "properties": { 131 | "endIpAddress": "0.0.0.0", 132 | "startIpAddress": "0.0.0.0" 133 | } 134 | } 135 | ] 136 | }, 137 | { 138 | "type": "Microsoft.Storage/storageAccounts", 139 | "name": "[variables('storageAccountName')]", 140 | "apiVersion": "2016-01-01", 141 | "location": "[resourceGroup().location]", 142 | "sku": { 143 | "name": "[parameters('Storage_Account_Tier')]" 144 | }, 145 | "kind": "Storage", 146 | "properties": { 147 | } 148 | }, 149 | { 150 | "apiVersion": "2016-03-01", 151 | "name": "[variables('hostingPlanName')]", 152 | "type": "Microsoft.Web/serverfarms", 153 | "location": "[resourceGroup().location]", 154 | "tags": { 155 | "displayName": "HostingPlan" 156 | }, 157 | "sku": { 158 | "name": "[parameters('App_Tier')]", 159 | "capacity": "[parameters('App_Capacity')]" 160 | }, 161 | "properties": { 162 | "name": "[variables('hostingPlanName')]" 163 | } 164 | }, 165 | { 166 | "apiVersion": "2016-03-01", 167 | "name": "[variables('webSiteName')]", 168 | "type": "Microsoft.Web/sites", 169 | "location": "[resourceGroup().location]", 170 | "dependsOn": [ 171 | "[variables('hostingPlanName')]", 172 | "[variables('databaseName')]" 173 | ], 174 | "tags": { 175 | "[concat('hidden-related:', resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName')))]": "empty", 176 | "displayName": "Website" 177 | }, 178 | "properties": { 179 | "name": "[variables('webSiteName')]", 180 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]" 181 | }, 182 | "resources": [ 183 | { 184 | "apiVersion": "2015-08-01", 185 | "name": "web", 186 | "type": "sourcecontrols", 187 | "dependsOn": [ 188 | "[resourceId('Microsoft.Web/Sites', variables('webSiteName'))]" 189 | ], 190 | "properties": { 191 | "RepoUrl": "[variables('repoURL')]", 192 | "branch": "[variables('branch')]", 193 | "IsManualIntegration": true 194 | } 195 | }, 196 | { 197 | "apiVersion": "2016-03-01", 198 | "name": "appsettings", 199 | "type": "config", 200 | "dependsOn": [ 201 | "[resourceId('Microsoft.Web/Sites', variables('webSiteName'))]" 202 | ], 203 | "properties": { 204 | "DB_SERVER":"[reference(concat('Microsoft.Sql/servers/', variables('sqlserverName'))).fullyQualifiedDomainName]", 205 | "DB_NAME":"[variables('databaseName')]", 206 | "DB_EMAIL":"[parameters('Admin_Email')]", 207 | "DB_USER":"[parameters('Admin_Username')]", 208 | "DB_PASSWORD":"[parameters('Admin_Password')]", 209 | "STORAGE_ACCOUNT":"[variables('storageAccountName')]", 210 | "STORAGE_KEY":"[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value]", 211 | "GOOGLE_CLIENT_ID":"[parameters('GOOGLE_CLIENT_ID')]", 212 | "GOOGLE_CLIENT_SECRET":"[parameters('GOOGLE_CLIENT_SECRET')]", 213 | "GOOGLE_CALLBACK_URL":"[concat('https://',concat(variables('webSiteName'),'.azurewebsites.net/.auth/login/google/callback'))]" 214 | } 215 | }, 216 | { 217 | "apiVersion": "2016-03-01", 218 | "type": "config", 219 | "name": "connectionstrings", 220 | "dependsOn": [ 221 | "[variables('webSiteName')]" 222 | ], 223 | "properties": { 224 | "DefaultConnection": { 225 | "value": "[concat('Data Source=tcp:', reference(concat('Microsoft.Sql/servers/', variables('sqlserverName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', variables('databaseName'), ';User Id=', parameters('Admin_Username'), '@', reference(concat('Microsoft.Sql/servers/', variables('sqlserverName'))).fullyQualifiedDomainName, ';Password=', parameters('Admin_Password'), ';')]", 226 | "type": "SQLAzure" 227 | } 228 | } 229 | } 230 | ] 231 | }, 232 | { 233 | "apiVersion": "2015-04-01", 234 | "name": "[concat(variables('hostingPlanName'), '-', resourceGroup().name)]", 235 | "type": "Microsoft.Insights/autoscalesettings", 236 | "location": "[resourceGroup().location]", 237 | "tags": { 238 | "[concat('hidden-link:', resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName')))]": "Resource", 239 | "displayName": "AutoScaleSettings" 240 | }, 241 | "dependsOn": [ 242 | "[variables('hostingPlanName')]" 243 | ], 244 | "properties": { 245 | "profiles": [ 246 | { 247 | "name": "Default", 248 | "capacity": { 249 | "minimum": 1, 250 | "maximum": 2, 251 | "default": 1 252 | }, 253 | "rules": [ 254 | { 255 | "metricTrigger": { 256 | "metricName": "CpuPercentage", 257 | "metricResourceUri": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 258 | "timeGrain": "PT1M", 259 | "statistic": "Average", 260 | "timeWindow": "PT10M", 261 | "timeAggregation": "Average", 262 | "operator": "GreaterThan", 263 | "threshold": 80.0 264 | }, 265 | "scaleAction": { 266 | "direction": "Increase", 267 | "type": "ChangeCount", 268 | "value": 1, 269 | "cooldown": "PT10M" 270 | } 271 | }, 272 | { 273 | "metricTrigger": { 274 | "metricName": "CpuPercentage", 275 | "metricResourceUri": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 276 | "timeGrain": "PT1M", 277 | "statistic": "Average", 278 | "timeWindow": "PT1H", 279 | "timeAggregation": "Average", 280 | "operator": "LessThan", 281 | "threshold": 60.0 282 | }, 283 | "scaleAction": { 284 | "direction": "Decrease", 285 | "type": "ChangeCount", 286 | "value": 1, 287 | "cooldown": "PT1H" 288 | } 289 | } 290 | ] 291 | } 292 | ], 293 | "enabled": false, 294 | "name": "[concat(variables('hostingPlanName'), '-', resourceGroup().name)]", 295 | "targetResourceUri": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]" 296 | } 297 | }, 298 | { 299 | "apiVersion": "2015-04-01", 300 | "name": "[concat('ServerErrors ', variables('webSiteName'))]", 301 | "type": "Microsoft.Insights/alertrules", 302 | "location": "[resourceGroup().location]", 303 | "dependsOn": [ 304 | "[variables('webSiteName')]" 305 | ], 306 | "tags": { 307 | "[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('webSiteName')))]": "Resource", 308 | "displayName": "ServerErrorsAlertRule" 309 | }, 310 | "properties": { 311 | "name": "[concat('ServerErrors ', variables('webSiteName'))]", 312 | "description": "[concat(variables('webSiteName'), ' has some server errors, status code 5xx.')]", 313 | "isEnabled": false, 314 | "condition": { 315 | "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", 316 | "dataSource": { 317 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", 318 | "resourceUri": "[resourceId('Microsoft.Web/sites', variables('webSiteName'))]", 319 | "metricName": "Http5xx" 320 | }, 321 | "operator": "GreaterThan", 322 | "threshold": 0.0, 323 | "windowSize": "PT5M" 324 | }, 325 | "action": { 326 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", 327 | "sendToServiceOwners": true, 328 | "customEmails": [ ] 329 | } 330 | } 331 | }, 332 | { 333 | "apiVersion": "2015-04-01", 334 | "name": "[concat('ForbiddenRequests ', variables('webSiteName'))]", 335 | "type": "Microsoft.Insights/alertrules", 336 | "location": "[resourceGroup().location]", 337 | "dependsOn": [ 338 | "[variables('webSiteName')]" 339 | ], 340 | "tags": { 341 | "[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('webSiteName')))]": "Resource", 342 | "displayName": "ForbiddenRequestsAlertRule" 343 | }, 344 | "properties": { 345 | "name": "[concat('ForbiddenRequests ', variables('webSiteName'))]", 346 | "description": "[concat(variables('webSiteName'), ' has some requests that are forbidden, status code 403.')]", 347 | "isEnabled": false, 348 | "condition": { 349 | "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", 350 | "dataSource": { 351 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", 352 | "resourceUri": "[resourceId('Microsoft.Web/sites', variables('webSiteName'))]", 353 | "metricName": "Http403" 354 | }, 355 | "operator": "GreaterThan", 356 | "threshold": 0, 357 | "windowSize": "PT5M" 358 | }, 359 | "action": { 360 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", 361 | "sendToServiceOwners": true, 362 | "customEmails": [ ] 363 | } 364 | } 365 | }, 366 | { 367 | "apiVersion": "2015-04-01", 368 | "name": "[concat('CPUHigh ', variables('hostingPlanName'))]", 369 | "type": "Microsoft.Insights/alertrules", 370 | "location": "[resourceGroup().location]", 371 | "dependsOn": [ 372 | "[variables('hostingPlanName')]" 373 | ], 374 | "tags": { 375 | "[concat('hidden-link:', resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName')))]": "Resource", 376 | "displayName": "CPUHighAlertRule" 377 | }, 378 | "properties": { 379 | "name": "[concat('CPUHigh ', variables('hostingPlanName'))]", 380 | "description": "[concat('The average CPU is high across all the instances of ', variables('hostingPlanName'))]", 381 | "isEnabled": false, 382 | "condition": { 383 | "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", 384 | "dataSource": { 385 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", 386 | "resourceUri": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 387 | "metricName": "CpuPercentage" 388 | }, 389 | "operator": "GreaterThan", 390 | "threshold": 90, 391 | "windowSize": "PT15M" 392 | }, 393 | "action": { 394 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", 395 | "sendToServiceOwners": true, 396 | "customEmails": [ ] 397 | } 398 | } 399 | }, 400 | { 401 | "apiVersion": "2015-04-01", 402 | "name": "[concat('LongHttpQueue ', variables('hostingPlanName'))]", 403 | "type": "Microsoft.Insights/alertrules", 404 | "location": "[resourceGroup().location]", 405 | "dependsOn": [ 406 | "[variables('hostingPlanName')]" 407 | ], 408 | "tags": { 409 | "[concat('hidden-link:', resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName')))]": "Resource", 410 | "displayName": "AutoScaleSettings" 411 | }, 412 | "properties": { 413 | "name": "[concat('LongHttpQueue ', variables('hostingPlanName'))]", 414 | "description": "[concat('The HTTP queue for the instances of ', variables('hostingPlanName'), ' has a large number of pending requests.')]", 415 | "isEnabled": false, 416 | "condition": { 417 | "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", 418 | "dataSource": { 419 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", 420 | "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', variables('hostingPlanName'))]", 421 | "metricName": "HttpQueueLength" 422 | }, 423 | "operator": "GreaterThan", 424 | "threshold": 100.0, 425 | "windowSize": "PT5M" 426 | }, 427 | "action": { 428 | "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", 429 | "sendToServiceOwners": true, 430 | "customEmails": [ ] 431 | } 432 | } 433 | }, 434 | { 435 | "apiVersion": "2015-05-01", 436 | "name": "[concat('AppInsights', variables('webSiteName'))]", 437 | "type": "Microsoft.Insights/components", 438 | "location": "[resourceGroup().location]", 439 | "dependsOn": [ 440 | "[variables('webSiteName')]" 441 | ], 442 | "tags": { 443 | "[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('webSiteName')))]": "Resource", 444 | "displayName": "AppInsightsComponent" 445 | }, 446 | "properties": { 447 | "ApplicationId": "[variables('webSiteName')]" 448 | } 449 | } 450 | ], 451 | "outputs": { 452 | "siteUri": { 453 | "type": "string", 454 | "value": "[reference(concat('Microsoft.Web/sites/', variables('webSiteName'))).hostnames[0]]" 455 | }, 456 | "sqlSvrFqdn": { 457 | "type": "string", 458 | "value": "[reference(concat('Microsoft.Sql/servers/', variables('sqlserverName'))).fullyQualifiedDomainName]" 459 | }, 460 | "storageAccountName": { 461 | "type": "string", 462 | "value": "[variables('storageAccountName')]" 463 | } 464 | 465 | } 466 | 467 | } -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var localConfigPath = path.join(__dirname, "config.private.json"); 6 | var localConfig = fs.existsSync(localConfigPath) && require(localConfigPath) || {}; 7 | 8 | var config = { 9 | sql: { 10 | server: localConfig.sql && localConfig.sql.server || process.env.DB_SERVER, 11 | userName: localConfig.sql && localConfig.sql.userName || process.env.DB_USER, 12 | password: localConfig.sql && localConfig.sql.password || process.env.DB_PASSWORD, 13 | options: { 14 | database: localConfig.sql && localConfig.sql.options && localConfig.sql.options.database || process.env.DB_NAME, 15 | encrypt: true 16 | } 17 | }, 18 | storage: { 19 | account: localConfig.storage && localConfig.storage.account || process.env.STORAGE_ACCOUNT, 20 | key: localConfig.storage && localConfig.storage.key || process.env.STORAGE_KEY 21 | }, 22 | auth: { 23 | google: { 24 | clientID: localConfig.auth && localConfig.auth.google && localConfig.auth.google.clientID || process.env.GOOGLE_CLIENT_ID, 25 | clientSecret: localConfig.auth && localConfig.auth.google && localConfig.auth.google.clientSecret || process.env.GOOGLE_CLIENT_SECRET, 26 | callbackURL: localConfig.auth && localConfig.auth.google && localConfig.auth.google.callbackURL || process.env.GOOGLE_CALLBACK_URL 27 | } 28 | } 29 | }; 30 | 31 | if (!config.sql.server) throw new Error('Sql server was not provided, please add DB_SERVER to environment variables'); 32 | if (!config.sql.userName) throw new Error('Sql user was not provided, please add DB_USER to environment variables'); 33 | if (!config.sql.password) throw new Error('password for db was not provided, please add DB_PASSWORD to environment variables'); 34 | if (!config.sql.options.database) throw new Error('db name was not provided, please add DB_NAME to environment variables'); 35 | 36 | 37 | if (!config.storage.account) throw new Error('storage account was not provided, please add STORAGE_ACCOUNT to environment variables'); 38 | if (!config.storage.key) throw new Error('storage key was not provided, please add STORAGE_KEY to environment variables'); 39 | 40 | if (!config.auth.google.clientID) throw new Error('google client Id was not provided, please add GOOGLE_CLIENT_ID to environment variables'); 41 | if (!config.auth.google.clientSecret) throw new Error('google client secret was not provided, please add GOOGLE_CLIENT_SECRET to environment variables'); 42 | if (!config.auth.google.callbackURL) throw new Error('google callback URL was not provided, please add GOOGLE_CALLBACK_URL to environment variables'); 43 | 44 | 45 | module.exports = config; 46 | -------------------------------------------------------------------------------- /config/sample.config.private.json: -------------------------------------------------------------------------------- 1 | { 2 | "sql": { 3 | "server": "yourSqlServerName", 4 | "userName": "yourSqlServerUsername", 5 | "password": "yourSqlServerPassword", 6 | "options": { 7 | "database": "yourSqlDbName", 8 | "encrypt": true 9 | } 10 | }, 11 | "storage": { 12 | "account": "yourStorageAccount", 13 | "key": "yourStorageAccountKey" 14 | }, 15 | "auth" : { 16 | "google": { 17 | "clientID": "yourGoogleClientId", 18 | "clientSecret": "yourGoogleClientSecret", 19 | "callbackURL": "yourGoogleCallbackUrl" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | :: ---------------------- 4 | :: KUDU Deployment Script 5 | :: Version: 1.0.3 6 | :: ---------------------- 7 | 8 | :: Prerequisites 9 | :: ------------- 10 | 11 | :: Verify node.js installed 12 | where node 2>nul >nul 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. 15 | goto error 16 | ) 17 | 18 | :: Setup 19 | :: ----- 20 | 21 | setlocal enabledelayedexpansion 22 | 23 | SET ARTIFACTS=%~dp0%..\artifacts 24 | 25 | IF NOT DEFINED DEPLOYMENT_SOURCE ( 26 | SET DEPLOYMENT_SOURCE=%~dp0%. 27 | ) 28 | 29 | IF NOT DEFINED DEPLOYMENT_TARGET ( 30 | SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot 31 | ) 32 | 33 | IF NOT DEFINED NEXT_MANIFEST_PATH ( 34 | SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest 35 | 36 | IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( 37 | SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest 38 | ) 39 | ) 40 | 41 | IF NOT DEFINED KUDU_SYNC_CMD ( 42 | :: Install kudu sync 43 | echo Installing Kudu Sync 44 | call npm install kudusync -g --silent 45 | IF !ERRORLEVEL! NEQ 0 goto error 46 | 47 | :: Locally just running "kuduSync" would also work 48 | SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd 49 | ) 50 | goto Deployment 51 | 52 | :: Utility Functions 53 | :: ----------------- 54 | 55 | :SelectNodeVersion 56 | 57 | IF DEFINED KUDU_SELECT_NODE_VERSION_CMD ( 58 | :: The following are done only on Windows Azure Websites environment 59 | call %KUDU_SELECT_NODE_VERSION_CMD% "%DEPLOYMENT_SOURCE%" "%DEPLOYMENT_TARGET%" "%DEPLOYMENT_TEMP%" 60 | IF !ERRORLEVEL! NEQ 0 goto error 61 | 62 | IF EXIST "%DEPLOYMENT_TEMP%\__nodeVersion.tmp" ( 63 | SET /p NODE_EXE=<"%DEPLOYMENT_TEMP%\__nodeVersion.tmp" 64 | IF !ERRORLEVEL! NEQ 0 goto error 65 | ) 66 | 67 | IF EXIST "%DEPLOYMENT_TEMP%\__npmVersion.tmp" ( 68 | SET /p NPM_JS_PATH=<"%DEPLOYMENT_TEMP%\__npmVersion.tmp" 69 | IF !ERRORLEVEL! NEQ 0 goto error 70 | ) 71 | 72 | IF NOT DEFINED NODE_EXE ( 73 | SET NODE_EXE=node 74 | ) 75 | 76 | SET NPM_CMD="!NODE_EXE!" "!NPM_JS_PATH!" 77 | ) ELSE ( 78 | SET NPM_CMD=npm 79 | SET NODE_EXE=node 80 | ) 81 | 82 | goto :EOF 83 | 84 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 85 | :: Deployment 86 | :: ---------- 87 | 88 | :Deployment 89 | echo Handling node.js deployment. 90 | 91 | :: 1. KuduSync 92 | IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( 93 | call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" 94 | IF !ERRORLEVEL! NEQ 0 goto error 95 | ) 96 | 97 | :: 2. Select node version 98 | call :SelectNodeVersion 99 | 100 | :: 3. Install npm packages 101 | IF EXIST "%DEPLOYMENT_TARGET%\package.json" ( 102 | pushd "%DEPLOYMENT_TARGET%" 103 | call :ExecuteCmd !NPM_CMD! install --production 104 | IF !ERRORLEVEL! NEQ 0 goto error 105 | popd 106 | ) 107 | 108 | :: 4. Install bower packages 109 | IF EXIST "%DEPLOYMENT_TARGET%\public\bower.json" ( 110 | pushd "%DEPLOYMENT_TARGET%\public" 111 | call ..\node_modules\.bin\bower install --force-latest 112 | IF !ERRORLEVEL! NEQ 0 goto error 113 | popd 114 | ) 115 | 116 | 117 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 118 | 119 | :: Post deployment stub 120 | IF DEFINED POST_DEPLOYMENT_ACTION call "%POST_DEPLOYMENT_ACTION%" 121 | IF !ERRORLEVEL! NEQ 0 goto error 122 | 123 | goto end 124 | 125 | :: Execute command routine that will echo out when error 126 | :ExecuteCmd 127 | setlocal 128 | set _CMD_=%* 129 | echo calling command: %_CMD_% 130 | call %_CMD_% 131 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 132 | exit /b %ERRORLEVEL% 133 | 134 | :error 135 | endlocal 136 | echo An error has occurred during web site deployment. 137 | call :exitSetErrorLevel 138 | call :exitFromFunction 2>nul 139 | 140 | :exitSetErrorLevel 141 | exit /b 1 142 | 143 | :exitFromFunction 144 | () 145 | 146 | :end 147 | endlocal 148 | echo Finished successfully. 149 | -------------------------------------------------------------------------------- /deploy/azure.md: -------------------------------------------------------------------------------- 1 | # Deploying Video Tagging App to Azure 2 | Follow these instructions to deploy the demo app to Azure. 3 | 4 | ## Allocate resources in Azure Management Portal 5 | 1. Open [Azure Management Portal] (https://ms.portal.azure.com) 6 | 2. Press the `+` sign and search for `Web App + SQL`. Select it, and press `Create`: 7 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img10.png) 8 | 3. Fill in the App Name, Database and create a new Resource Group to contain all of the related resources: 9 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img20.png) 10 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img30.png) 11 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img40.png) 12 | 4. Click `Create`, and wait while deployment is running. When deployment is ready, you'll be automatically redirected to the resource group and see the list of the resources allocated for you: 13 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img50.png) 14 | 5. Before setting up the resources, we need to create a Storage Account where the videos are being stored. Press the `+` sign, and search for a `Storage Account`. Click `Create`: 15 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img60.png) 16 | 6. Type a name, make sure to selected the same resource group, and the same region for your storage account: 17 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img70.png) 18 | 7. Click `Create` and wait until deployment is ready. 19 | 8. After storage account is created, click the Keys icon on the top bar and copy the storage account `Name` and `Access Key` (Key 1) to a temporary file. We'll use it in a bit. 20 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img80.png) 21 | 9. Go back to your resource group by clicking on it's tile on the Dashboard, or looking for it under the `Resource Groups` menu item on the left. 22 | You should now see all resources: 23 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img90.png) 24 | 25 | ## Setting up SQL Server schema: 26 | 1. Fork the [Video Tagging Tool repository](https://github.com/CatalystCode/VideoTaggingTool) to your own GitHub account. 27 | 2. Download the `schema.sql` file located under `storage/sql` directory. Edit the file, scroll to the end of the file and edit the last line to add you as the first user of the tool: 28 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img100.png) 29 | 3. Next step will be to setup the environment to host the website: 30 | * First we need to enable access to the Sql server from your machine IP, so that you can connect and create the schema. Click `videotaggingsrv` --> `All Settings` --> `Firewall` and add your IP. For the sake of the demo, I'm enabling all IP range. Save your changes. 31 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img115.png) 32 | * Use your favorite SQL Server client UI to connect to the SQL server. I'm using [Sql Server Management Studio] (https://msdn.microsoft.com/en-us/library/mt238290.aspx). You can also use Visual Studio in case you're using Windows. In case of other OS, look for Sql Server UI Client that can run on your OS. [DBeaver] (http://dbeaver.jkiss.org/download/) is a nice option for MacOS / Ubuntu. 33 | * Use the SQL server settings, as defined when you created the SQL server. Click the `SQL Server` --> `Settings` to get the SQL server host name, and use the user and password you defined with the SQL Server Authentication scheme: 34 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img110.png) 35 | 4. After connecting, you'll see the database that was provisioned for you. Right-click and select `New Query`: 36 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img120.png) 37 | 5. Copy-Paste the content of the `Schema.sql` file to this window, and click `F5` to execute the script and create all of the db objects: 38 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img130.png) 39 | 40 | ## Enable Google Authentication: 41 | 1. Browse to [Google Developer Console](https://console.developers.google.com/?pli=1). 42 | 2. Under `Use Google APIs`, click `Enable and Manage APIs` link. 43 | 3. Select `Google+ API` and click `Enable`. 44 | 4. Open the `Credentials` tab. Click the `Create Credentials` select box and select `OAuth client ID`. 45 | 5. Select `Web Application` option from the menu and fill in the following details: 46 | * In the `Authorized Javascript origins`, add the Url for your website. This can be copied from the web app properties in the Azure portal. 47 | * In the `Authorized redirect URIs`, copy the same Url (**with https scheme**), and add `/.auth/login/google/callback` as demonstrated below: 48 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img140.png) 49 | * Click the `Create` button. 50 | 6. You'll get a `client Id` and a `client secret`. Copy these strings to a temporary file. We'll use it in a bit: 51 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img150.png) 52 | 53 | ## Setting web app environment variables: 54 | Next step will be to set up the web app environment variables. Go back to the MS Azure portal, 55 | and click the Web App icon on the resource group list. 56 | 57 | Click `All Settings` --> `Application Settings` and add the following entries under the `App Settings` section. 58 | **Make sure you're under the `Application Settings` section and not under the `Connection Strings` section**: 59 | 60 | * `DB_SERVER` - The SQL server host name (`videotaggingsrv.database.windows.net` in our case) 61 | * `DB_NAME`- The database name (`videoTaggingDemoDb` in our case) 62 | * `DB_USER` - The SQL server user name (`video` in our case) 63 | * `DB_PASSWORD`- The SQL server password 64 | * `STORAGE_ACCOUNT`- The storage account (`videotaggingdemo` in our case) 65 | * `STORAGE_KEY`- The storage key 66 | * `GOOGLE_CLIENT_ID`- Google's client Id 67 | * `GOOGLE_CLIENT_SECRET`- Google client secret 68 | * `GOOGLE_CALLBACK_URL`- The website URL (**using https scheme**) + `/.auth/login/google/callback` (`https://video-tagging-demo.azurewebsites.net/.auth/login/google/callback` in our case). 69 | 70 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img160.png) 71 | 72 | ## Setting up Continuous Integration and deploying the app from Github 73 | 1. Click the `Continuous deployment` option under the `Publishing` option for the Web App. 74 | 2. Select `Github` as the source and set up your authentication. 75 | 3. Choose the forked repository and select the `master` branch. 76 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img170.png) 77 | Click `OK`, and hope for the best :-) 78 | 79 | After a few minutes, if everything went well, you should be able to point your browser to the website URL and log in to the tool. 80 | Since you're the only Admin user, you'll be able to add more users under the `Users` tab. 81 | 82 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img180.png) 83 | 84 | 85 | Enjoy! 86 | -------------------------------------------------------------------------------- /deploy/img/img10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img10.png -------------------------------------------------------------------------------- /deploy/img/img100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img100.png -------------------------------------------------------------------------------- /deploy/img/img110.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img110.png -------------------------------------------------------------------------------- /deploy/img/img115.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img115.png -------------------------------------------------------------------------------- /deploy/img/img120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img120.png -------------------------------------------------------------------------------- /deploy/img/img130.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img130.png -------------------------------------------------------------------------------- /deploy/img/img140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img140.png -------------------------------------------------------------------------------- /deploy/img/img150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img150.png -------------------------------------------------------------------------------- /deploy/img/img160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img160.png -------------------------------------------------------------------------------- /deploy/img/img170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img170.png -------------------------------------------------------------------------------- /deploy/img/img180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img180.png -------------------------------------------------------------------------------- /deploy/img/img20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img20.png -------------------------------------------------------------------------------- /deploy/img/img30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img30.png -------------------------------------------------------------------------------- /deploy/img/img40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img40.png -------------------------------------------------------------------------------- /deploy/img/img50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img50.png -------------------------------------------------------------------------------- /deploy/img/img60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img60.png -------------------------------------------------------------------------------- /deploy/img/img70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img70.png -------------------------------------------------------------------------------- /deploy/img/img80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img80.png -------------------------------------------------------------------------------- /deploy/img/img90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/deploy/img/img90.png -------------------------------------------------------------------------------- /deploy/local.md: -------------------------------------------------------------------------------- 1 | # Running the Video Tagging App locally 2 | Follow these instructions to run the demo app on you local machine. 3 | 4 | ## Enable Google Authentication: 5 | 1. Browse to [Google Developer Console](https://console.developers.google.com/?pli=1). 6 | 2. Under `Use Google APIs`, click `Enable and Manage APIs` link. 7 | 3. Select `Google+ API` and click `Enable`. 8 | 4. Open the `Credentials` tab. Click the `Create Credentials` select box and select `OAuth client ID`. 9 | 5. Select `Web Application` option from the menu and fill in the following details: 10 | * In the `Authorized Javascript origins`, add the Url for your website: `http://localhost:3000` in this case. 11 | * In the `Authorized redirect URIs`, add the callback Url: `http://localhost:3000/.auth/login/google/callback` in this case. 12 | * Click the `Create` button. 13 | 6. You'll get a `client Id` and a `client secret`. Copy these strings to a temporary file. We'll use it in a bit. 14 | 15 | ## SQL Server 16 | We're using Sql DB to keep our app data. You'll need to create your own Sql DB, either locally or in the cloud. 17 | You can use Sql Express (which is [free] (https://www.microsoft.com/en-gb/download/details.aspx?id=42299)) for running the app locally, or create a Sql database in [Azure] (https://azure.microsoft.com/en-gb/services/sql-database). 18 | After creating your DB, you'll need to run a Sql script that will deploy the DB schema. 19 | 20 | 1. Download the `schema.sql` file located under `storage/sql` directory. Edit the file, scroll to the end of the file and edit the last line to add you as the first user of the tool: 21 | 22 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img100.png) 23 | 24 | 2. If you're using Azure SQL, we need to enable access to the Sql server from your machine IP, so that you can connect and create the schema. Click the SQL Server instance on MS Azure portal --> `All Settings` --> `Firewall` and add your IP. For the sake of the demo, I'm enabling all IP range. Save your changes. 25 | 26 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img115.png) 27 | 28 | * Use your favorite SQL Server client UI to connect to the SQL server. I'm using [Sql Server Management Studio] (https://msdn.microsoft.com/en-us/library/mt238290.aspx). You can also use Visual Studio in case you're using Windows. In case of other OS, look for Sql Server UI Client that can run on your OS. [DBeaver] (http://dbeaver.jkiss.org/download/) is a nice option for MacOS / Ubuntu. 29 | * Use the SQL server settings, as defined when you created the SQL server. Click the `SQL Server` --> `Settings` to get the SQL server host name, and use the user and password you defined with the SQL Server Authentication scheme: 30 | 31 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img110.png) 32 | 33 | 3. After connecting, you'll see the database that was provisioned for you. Right-click and select `New Query`: 34 | 35 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img120.png) 36 | 37 | 4. Copy-Paste the content of the `Schema.sql` file to this window, and click `F5` to execute the script and create all of the db objects: 38 | 39 | ![screenshot](https://github.com/CatalystCode/VideoTaggingTool/raw/master/deploy/img/img130.png) 40 | 41 | ## Storage Account 42 | We're using Azure Storage Accont to store the videos. 43 | You can use the local storage account provided as part of Azure SDK, or provision a new account in Azure. 44 | 45 | ## Configuration 46 | Create a `/config/config.private.json` file with your configuration and secret keys when running locally. Use `config/sample.config.private.json` file as a reference. 47 | You can start by copying this file to `/config/config.private.json` and then edit the content accordingly: 48 | * `sql.server`- your Sql server name 49 | * `sql.userName`- your Sql server name 50 | * `sql.password`- your Sql password 51 | * `sql.options.database`- your Sql database name 52 | * `storage.account`- your Azure storage account name 53 | * `storage.key`- your Azure storage account key 54 | * `auth.google.clientID`- your google client Id 55 | * `auth.google.clientSecret`- your google client secret 56 | * `auth.google.callbackURL`- your google client URL- this is the URL that will be called with the authentication token after the user provides his consent. Use `http://localhost:3000/.auth/login/google/callback` when working locally. 57 | 58 | > When deploying the app to the cloud, it is recommended to use the environment variables instead of the config file. 59 | > Take a look at `/config/index.js` file to get the names of the environment variables corresponding to the configuration items described above. 60 | 61 | 62 | ## Installing node modules and web app bower modules: 63 | * Run `npm install` from the root directory 64 | * Run `bower install` from `public` directory (if you don’t have bower, install it by running `npm install bower -g`) 65 | 66 | ## Running the app 67 | Run `npm start` and browse to `http://localhost:3000` 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VideoTaggingTool", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "node ./storage/sql/ensureSchema.js", 7 | "start": "node server.js" 8 | }, 9 | "description": "Tool for tagging videos frame by frame", 10 | "author": { 11 | "name": "Ami Turgman", 12 | "email": "ami.turgman@microsoft.com" 13 | }, 14 | "engines": { 15 | "node": "0.10.32" 16 | }, 17 | "dependencies": { 18 | "azure-storage": "^0.7.0", 19 | "body-parser": "~1.8.1", 20 | "bower": "^1.7.2", 21 | "connect-flash": "^0.1.1", 22 | "cookie-parser": "^1.4.0", 23 | "debug": "~2.0.0", 24 | "express": "~4.9.0", 25 | "express-session": "^1.12.1", 26 | "googleapis": "^2.1.7", 27 | "morgan": "^1.6.1", 28 | "multiparty": "^4.1.2", 29 | "passport": "^0.3.2", 30 | "passport-google-oauth": "^0.2.0", 31 | "tedious": "^1.13.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VideoTaggingTool", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Ami Turgman " 6 | ], 7 | "license": "MIT", 8 | "private": true, 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "angular": "~1.3.14", 18 | "angular-route": "~1.3.14", 19 | "angular-sanitize": "~1.3.15", 20 | "webcomponentsjs": "~0.7.20", 21 | "polymer": "~1.2.3", 22 | "moment": "~2.10.6", 23 | "bootstrap": "~3.3.6", 24 | "jquery": "~2.1.4", 25 | "underscore": "~1.8.3", 26 | "bootstrap-tokenfield": "~0.12.1", 27 | "jquery-ui": "~1.11.4", 28 | "video-tagging": "~0.0.24" 29 | }, 30 | "resolutions": { 31 | "bootstrap": "~3.3.6", 32 | "jquery": "~2.1.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial; 3 | font-size: 14px; 4 | } 5 | 6 | h1 { 7 | color: #3c3c3c; 8 | } 9 | 10 | .footer { 11 | padding-top: 10px; 12 | padding-bottom: 40px; 13 | margin-top: 50px; 14 | border-top: 1px solid #eee; 15 | } 16 | 17 | .nav-justified { 18 | background-color: #eee; 19 | border: 1px solid #ccc; 20 | border-radius: 5px; 21 | } 22 | 23 | .nav-justified > li > a { 24 | padding-top: 15px; 25 | padding-bottom: 15px; 26 | margin-bottom: 0; 27 | font-weight: bold; 28 | color: #777; 29 | text-align: center; 30 | background-color: #e5e5e5; /* Old browsers */ 31 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e5e5e5)); 32 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e5e5e5 100%); 33 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e5e5e5 100%); 34 | background-image: -webkit-gradient(linear, left top, left bottom, from(top), color-stop(0%, #f5f5f5), to(#e5e5e5)); 35 | background-image: linear-gradient(top, #f5f5f5 0%, #e5e5e5 100%); 36 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f5f5f5', endColorstr='#e5e5e5',GradientType=0 ); /* IE6-9 */ 37 | background-repeat: repeat-x; /* Repeat the gradient */ 38 | border-bottom: 1px solid #d5d5d5; 39 | } 40 | .nav-justified > .active > a, 41 | .nav-justified > .active > a:hover, 42 | .nav-justified > .active > a:focus { 43 | background-color: #ddd; 44 | background-image: none; 45 | -webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.15); 46 | box-shadow: inset 0 3px 7px rgba(0,0,0,.15); 47 | } 48 | .nav-justified > li:first-child > a { 49 | border-radius: 5px 5px 0 0; 50 | } 51 | .nav-justified > li:last-child > a { 52 | border-bottom: 0; 53 | border-radius: 0 0 5px 5px; 54 | } 55 | .nav { 56 | padding-top: 25px; 57 | } 58 | 59 | .nav li { 60 | display: inline; 61 | padding: 0px 3px 0px 7px; 62 | } 63 | .navbar-inverse { 64 | border-radius: 0px; 65 | } 66 | 67 | .navbar-left li { 68 | border-left: solid 1px #a2a5a3; 69 | line-height: 0.5; 70 | } 71 | .navbar-left li:first-child { 72 | border-left: none; 73 | } 74 | .navbar-nav > li > a { 75 | position: relative; 76 | display: block; 77 | text-align: center; 78 | padding: 0px 3px 0px 3px; 79 | line-height: 1; 80 | } 81 | 82 | .navbar-inverse .navbar-nav > .active > a, 83 | .navbar-inverse .navbar-nav > .active > a:focus, 84 | .navbar-inverse .navbar-nav > .active >a:hover { 85 | color: #fff; 86 | background-color: transparent; 87 | background-image: none; 88 | box-shadow: none; 89 | } 90 | 91 | .navbar-inverse .navbar-brand { 92 | float: right; 93 | color: #a2a5a3; 94 | } 95 | .navbar-inverse .navbar-brand a { 96 | text-shadow: none; 97 | } 98 | 99 | .navbar-inverse .navbar-brand:hover, 100 | .navbar-inverse .navbar-brand:focus { 101 | color: #a2a5a3; 102 | } 103 | .navbar-inverse .navbar-brand, 104 | .navbar-inverse .navbar-nav > li > a { 105 | text-shadow: none; 106 | } 107 | .navbar-inverse .navbar-nav>li>a { 108 | color: #a2a5a3; 109 | } 110 | .navbar-inverse .navbar-nav > li > a:hover, 111 | .navbar-inverse .navbar-nav > li > a:focus { 112 | color: #ffffff; 113 | } 114 | .navbar-logo { 115 | height: 58px; 116 | width: 144px; 117 | margin-right: 15px; 118 | } 119 | 120 | @media (min-width: 768px) { 121 | .navbar > .container .navbar-brand, .navbar >.container-fluid .navbar-brand { 122 | margin-left: -20px; 123 | } 124 | .nav-justified { 125 | max-height: 52px; 126 | } 127 | .nav-justified > li > a { 128 | border-right: 1px solid #d5d5d5; 129 | border-left: 1px solid #fff; 130 | } 131 | .nav-justified > li:first-child > a { 132 | border-left: 0; 133 | border-radius: 5px 0 0 5px; 134 | } 135 | .nav-justified > li:last-child > a { 136 | border-right: 0; 137 | border-radius: 0 5px 5px 0; 138 | } 139 | } 140 | 141 | .row { 142 | margin-left: 0px; 143 | } 144 | 145 | /* buttons */ 146 | .btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle):not(.btn-clear) { 147 | border-top-right-radius: 0; 148 | border-bottom-right-radius: 0; 149 | padding-left: 0px; 150 | } 151 | 152 | .btn-clear { 153 | color: #3c3c3c; 154 | margin-right: 5px; 155 | background-color: #d5d5d5; 156 | border: 0px; 157 | border-radius: 0px; 158 | } 159 | 160 | .btn-clear:hover { 161 | color: #3c3c3c; 162 | background-color: #a2a5a3; 163 | border: 0px; 164 | border-radius: 0px; 165 | } 166 | 167 | .alert-danger-clear { 168 | color: #ff0000; 169 | background-color: transparent; 170 | padding-top: 0px; 171 | border: 0px; 172 | box-shadow: none; 173 | } 174 | 175 | .alert-success-clear { 176 | color: #70ad47; 177 | background-color: transparent; 178 | padding-top: 0px; 179 | border: 0px; 180 | box-shadow: none; 181 | } 182 | 183 | /* button links & links */ 184 | 185 | .btn-group > .btn-link { 186 | padding : 0px 6px 0px 6px; 187 | border-left: 1px solid #a2a5a3; 188 | line-height: 1; 189 | } 190 | 191 | .btn-group > .btn-link:first-child { 192 | padding : 0px 6px 0px 6px; 193 | border-left: none; 194 | line-height: 1; 195 | } 196 | 197 | a, a:visited, .btn-link { 198 | color: #a2a5a3; 199 | text-decoration: none; 200 | } 201 | 202 | a:hover, 203 | .btn-link:hover, 204 | .btn-link:focus { 205 | color: #3c3c3c; 206 | text-decoration: none; 207 | } 208 | 209 | /* tables */ 210 | .table>tbody>tr>td, 211 | .table>tbody>tr>th, 212 | .table>tfoot>tr>td, 213 | .table>tfoot>tr>th, 214 | .table>thead>tr>td, 215 | .table>thead>tr>th { 216 | padding: 8px 8px 8px 0px; 217 | } 218 | 219 | .video-filters { 220 | padding-left: 0px; 221 | } 222 | 223 | /* Responsive: Portrait tablets and up */ 224 | @media screen and (min-width: 768px) { 225 | /* Remove the padding we set earlier */ 226 | .masthead, 227 | .marketing, 228 | .footer { 229 | padding-right: 0; 230 | padding-left: 0; 231 | } 232 | } 233 | 234 | #content { 235 | width: 100%; 236 | } 237 | 238 | h1 { 239 | margin-bottom: 1em; 240 | } 241 | 242 | .buttons { 243 | margin-bottom: 1.5em; 244 | } 245 | 246 | .buttons > div { 247 | margin-right: 1em; 248 | } 249 | 250 | th { 251 | margin: 3px; 252 | } 253 | 254 | .container { 255 | width: 100%; 256 | padding: 0px; 257 | } 258 | 259 | .container-inner { 260 | width: 70%; 261 | padding-left: 0px; 262 | padding-right: 0px; 263 | margin-left: auto; 264 | margin-right: auto; 265 | } 266 | 267 | .container-fluid { 268 | width: 70%; 269 | padding-left: 0px; 270 | padding-right: 0px; 271 | } 272 | 273 | span[ng-click] { 274 | margin-right: 10px; 275 | } 276 | 277 | .job-settings>tbody>tr>td{ 278 | padding-left: 1em; 279 | } 280 | 281 | button:focus, 282 | button:active, 283 | button:hover 284 | { 285 | outline:0px !important; 286 | -webkit-appearance:none; 287 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/public/favicon.ico -------------------------------------------------------------------------------- /public/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/public/images/ajax-loader.gif -------------------------------------------------------------------------------- /public/images/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/public/images/logo-text.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/public/images/logo.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | video tagging 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 72 | 73 |
74 |
75 | 76 | 83 |
84 | 85 | 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/VideoTaggingTool/30dfb5332c347f70402c58adfd53e721c77588fc/public/js/app.js -------------------------------------------------------------------------------- /public/js/controllers.js: -------------------------------------------------------------------------------- 1 |  2 | var videoTaggingAppControllers = angular.module('videoTaggingAppControllers', []); 3 | 4 | videoTaggingAppControllers 5 | 6 | .factory('state', ['$http', '$rootScope', function ($http, $rootScope) { 7 | 8 | var jobStatus = {}; 9 | 10 | $http({ method: 'GET', url: '/api/jobs/statuses' }) 11 | .success(function (result) { 12 | console.log('got statuses', result); 13 | result.statuses.forEach(function (status) { 14 | jobStatus[status.Name] = status.Id; 15 | }); 16 | }) 17 | .error(function (err) { 18 | console.error(err); 19 | }); 20 | 21 | $rootScope.jobSetup = { 22 | regionTypes: { values: ['Rectangle', 'Point'], default: 'Rectangle' }, 23 | multiRegions: { values: [{ id: '0', name: 'False' }, { id: '1', name: 'True' }], default: '1' }, 24 | regionSizes: { values: Array.apply(null, Array(100)).map(function (item, i) { return i + 1 }), default: 20 } 25 | }; 26 | 27 | $rootScope.jobStatuses = { 28 | Active: 1, 29 | Pending: 2, 30 | Approved: 3 31 | }; 32 | 33 | return { 34 | getJobStatusByName: function () { 35 | return jobStatus; 36 | } 37 | }; 38 | 39 | }]) 40 | 41 | .controller('AppController', ['$scope', '$rootScope', '$route', '$http', '$location', '$routeParams', 'state', function ($scope, $rootScope, $route, $http, $location, $routeParams, state) { 42 | 43 | $http({ method: 'GET', url: '/profile' }) 44 | .success(function (user) { 45 | console.log('got user profile', user); 46 | if(!user.Authorized) { 47 | console.warn('user authenticated but is not authorized to use the system'); 48 | return $location.path('/#/'); 49 | } 50 | $rootScope.user = user; 51 | }) 52 | .error(function (err, statusCode){ 53 | if(statusCode == 401) { 54 | console.warn('user is not logged in'); 55 | } 56 | else { 57 | console.error(err); 58 | } 59 | 60 | }); 61 | 62 | $scope.logout = function (){ 63 | $rootScope.user = user; 64 | } 65 | 66 | $scope.$route = $route; 67 | 68 | $rootScope.ajaxStart = function() { 69 | $rootScope.ajax = true; 70 | } 71 | $rootScope.ajaxCompleted = function() { 72 | $rootScope.ajax = false; 73 | } 74 | 75 | $rootScope.showError = function(err) { 76 | $rootScope.error = err; 77 | } 78 | 79 | $rootScope.showInfo = function(msg) { 80 | $rootScope.info = msg; 81 | } 82 | 83 | $rootScope.clearMessages = function() { 84 | $rootScope.error = ''; 85 | $rootScope.info = ''; 86 | } 87 | }]) 88 | 89 | .controller('JobsController', ['$scope', '$route', '$http', '$location', '$routeParams', function ($scope, $route, $http, $location, $routeParams) { 90 | 91 | $scope.clearMessages(); 92 | $scope.btnMeOrAll = 'me'; 93 | $scope.btnStatus = 'all'; 94 | 95 | $scope.ajaxStart(); 96 | $http({ method: 'GET', url: '/api/jobs/statuses' }) 97 | .success(function (result) { 98 | $scope.jobStatuses = result.statuses; 99 | $scope.ajaxCompleted(); 100 | }); 101 | 102 | var jobs; 103 | 104 | getJobsFromServer('/api/users/' + $scope.user.Id + '/jobs'); 105 | 106 | function getJobsFromServer(url) { 107 | console.log('getting jobs', url); 108 | 109 | $scope.ajaxStart(); 110 | $http({ method: 'GET', url: url }) 111 | .success(function (result) { 112 | jobs = result.jobs; 113 | _filter($scope.btnStatus); 114 | $scope.ajaxCompleted(); 115 | }) 116 | } 117 | 118 | function filterFetch(filter) { 119 | console.log('filter', filter); 120 | switch (filter) { 121 | case 'all': 122 | getJobsFromServer('/api/jobs'); 123 | break; 124 | default: 125 | getJobsFromServer('/api/users/' + $scope.user.Id + '/jobs'); 126 | } 127 | } 128 | 129 | $scope.filterFetch = function (filter) { 130 | $scope.btnMeOrAll = filter; 131 | filterFetch(filter); 132 | console.log(this); 133 | } 134 | 135 | $scope.filter = function (status) { 136 | $scope.btnStatus = status; 137 | _filter(status); 138 | } 139 | 140 | function _filter(status) { 141 | $scope.jobs = jobs.filter(function (job) { 142 | return status==='all' || job.StatusName == status; 143 | }); 144 | } 145 | 146 | $scope.count = function (status) { 147 | return jobs && jobs.filter(function (job) { 148 | return !status || job.StatusId == status; 149 | }).length || 0; 150 | } 151 | 152 | $scope.getConfig = function (job) { 153 | return JSON.stringify(job.Config, true, 2); 154 | } 155 | 156 | $scope.addJob = function () { 157 | $location.path('/jobs/0'); 158 | } 159 | 160 | $scope.editJob = function () { 161 | var jobId = this.job.JobId; 162 | $location.path('/jobs/' + jobId); 163 | } 164 | 165 | $scope.tagJob = function() { 166 | var jobId = this.job.JobId; 167 | $location.path('/jobs/' + jobId + '/tag'); 168 | } 169 | }]) 170 | 171 | .controller('UpsertJobController', ['$scope', '$http', '$location', '$routeParams', 'state', function ($scope, $http, $location, $routeParams, state) { 172 | 173 | $scope.clearMessages(); 174 | var defaultId = -1; 175 | $scope.jobId = defaultId; 176 | 177 | $scope.regiontype = $scope.jobSetup.regionTypes.default; 178 | $scope.multiregions = $scope.jobSetup.multiRegions.default; 179 | $scope.regionsize = $scope.jobSetup.regionSizes.default; 180 | $scope.selectedStatus = { Id: $scope.jobStatuses['Active'] }; 181 | 182 | if ($routeParams.id != 0) { 183 | $scope.ajaxStart(); 184 | $http({ method: 'GET', url: '/api/jobs/' + $routeParams.id }) 185 | .success(function (result) { 186 | console.log('jobData', result); 187 | $scope.jobId = result.job.Id; 188 | $scope.selectedVideo = result.video; 189 | $scope.selectedUser = result.user; 190 | $scope.selectedStatus = { Id: result.job.StatusId }; 191 | console.log(' $scope.selectedStatus', $scope.selectedStatus); 192 | $scope.description = result.job.Description; 193 | 194 | $scope.config = result.job.Config; 195 | $scope.regiontype = result.job.Config.regiontype; 196 | $scope.multiregions = result.job.Config.multiregions; 197 | $scope.regionsize = result.job.Config.regionsize; 198 | 199 | $scope.tags = result.job.Config && result.job.Config.tags && result.job.Config.tags.join(', '); 200 | $scope.ajaxCompleted(); 201 | }); 202 | } 203 | 204 | $scope.ajaxStart(); 205 | $http({ method: 'GET', url: '/api/jobs/statuses' }) 206 | .success(function (result) { 207 | console.log('got statuses', result); 208 | $scope.jobStatuses = result.statuses; 209 | $scope.ajaxCompleted(); 210 | }); 211 | 212 | $http({ method: 'GET', url: '/api/videos' }) 213 | .success(function (result) { 214 | $scope.videos = result.videos; 215 | console.log('videos', result); 216 | $scope.ajaxCompleted(); 217 | 218 | if ($routeParams.id == 0) { 219 | var videoId = $location.search()['videoId']; 220 | 221 | var selectedVideoArr = $scope.videos.filter(function(video){ 222 | return video.Id == videoId 223 | }); 224 | $scope.selectedVideo = selectedVideoArr.length && selectedVideoArr[0]; 225 | $scope.description = $scope.selectedVideo ? 'new job for video ' + $scope.selectedVideo.Name : 'new job'; 226 | } 227 | }); 228 | 229 | $http({ method: 'GET', url: '/api/users' }) 230 | .success(function (result) { 231 | $scope.users = result.users; 232 | console.log('users', result); 233 | $scope.ajaxCompleted(); 234 | }); 235 | 236 | $scope.submit = function () { 237 | 238 | $scope.clearMessages(); 239 | 240 | if(!$scope.selectedVideo || !$scope.selectedVideo.Id) return $scope.showError('video was not provided'); 241 | if(!$scope.selectedUser || !$scope.selectedUser.Id) return $scope.showError('user was not provided'); 242 | if(!$scope.description) return $scope.showError('description was not provided'); 243 | if(!$scope.selectedStatus || !$scope.selectedStatus.Id) return $scope.showError('status was not provided'); 244 | if(!$scope.regiontype) return $scope.showError('regiontype was not provided'); 245 | if(!$scope.multiregions) return $scope.showError('multiregions was not provided'); 246 | if(!$scope.regionsize) return $scope.showError('regionsize was not provided'); 247 | 248 | var data = { 249 | videoId: $scope.selectedVideo.Id, 250 | userId: $scope.selectedUser && $scope.selectedUser.Id, 251 | description: $scope.description, 252 | statusId: $scope.selectedStatus.Id, 253 | configJson: { 254 | regiontype: $scope.regiontype, 255 | multiregions: $scope.multiregions, 256 | regionsize: $scope.regionsize, 257 | tags: ($scope.tags && $scope.tags.split(',').map(function (tag) { return tag.trim(); })) || '' 258 | } 259 | }; 260 | 261 | if (!$scope.selectedUser || !$scope.selectedUser.Id) { 262 | return $scope.showError('user was not provided'); 263 | } 264 | 265 | if ($scope.jobId != defaultId) { 266 | data.id = $scope.jobId; 267 | } 268 | 269 | console.log('submitting', data); 270 | 271 | $scope.ajaxStart(); 272 | $http({ method: 'POST', url: '/api/jobs', data: data }) 273 | .success(function (result) { 274 | console.log('result', result); 275 | $scope.showInfo('job ' + result.jobId + ($scope.jobId == defaultId ? ' created' : ' modified') + ' successfully'); 276 | $scope.jobId = result.jobId; 277 | $scope.ajaxCompleted(); 278 | }) 279 | .error(function (err) { 280 | console.error(err); 281 | $scope.showError(err.message); 282 | $scope.ajaxCompleted(); 283 | }); 284 | } 285 | }]) 286 | 287 | .controller('UpsertUserController', ['$scope', '$http', '$location', '$routeParams', function ($scope, $http, $location, $routeParams) { 288 | 289 | $scope.clearMessages(); 290 | var defaultId = -1; 291 | $scope.userId = defaultId; 292 | 293 | if ($routeParams.id != 0) { 294 | $scope.ajaxStart(); 295 | $http({ method: 'GET', url: '/api/users/' + $routeParams.id }) 296 | .success(function (result) { 297 | console.log('userData', result); 298 | $scope.userId = result.Id; 299 | $scope.name = result.Name; 300 | $scope.email = result.Email; 301 | 302 | $scope.selectedRole = { Id: result.RoleId, name: result.RoleName }; 303 | $scope.ajaxCompleted(); 304 | }); 305 | } 306 | 307 | $scope.ajaxStart(); 308 | $http({ method: 'GET', url: '/api/roles' }) 309 | .success(function (result) { 310 | console.log('got statuses', result); 311 | $scope.roles = result.roles; 312 | $scope.ajaxCompleted(); 313 | }); 314 | 315 | 316 | $scope.submit = function () { 317 | 318 | $scope.clearMessages(); 319 | 320 | if(!$scope.name) return $scope.showError('name was not provided'); 321 | if(!$scope.email) return $scope.showError('email was not provided'); 322 | if(!$scope.selectedRole || !$scope.selectedRole.Id) return $scope.showError('role was not provided'); 323 | 324 | var data = { 325 | name: $scope.name, 326 | email: $scope.email, 327 | roleId: $scope.selectedRole.Id 328 | }; 329 | 330 | if ($scope.userId != defaultId) { 331 | data.id = $scope.userId; 332 | } 333 | 334 | console.log('submitting', data); 335 | $scope.ajaxStart(); 336 | $http({ method: 'POST', url: '/api/users', data: data }) 337 | .success(function (result) { 338 | console.log('result', result); 339 | $scope.showInfo('user ' + result.userId + ($scope.userId == defaultId ? ' created' : ' modified') + ' successfully'); 340 | $scope.userId = result.userId; 341 | $scope.ajaxCompleted(); 342 | }) 343 | .error(function (err) { 344 | console.error(err); 345 | $scope.showError(err.message); 346 | $scope.ajaxCompleted(); 347 | }); 348 | } 349 | }]) 350 | 351 | 352 | .controller('VideosController', ['$scope', '$route', '$http', '$location', '$routeParams', function ($scope, $route, $http, $location, $routeParams) { 353 | 354 | $scope.btnAllOrUnassigned = 0; 355 | getVideos(); 356 | 357 | $scope.ajaxStart(); 358 | $http({ method: 'GET', url: '/api/labels' }) 359 | .success(function (result) { 360 | $scope.labels = result.labels; 361 | }); 362 | 363 | function getVideos() { 364 | 365 | var selectedFilters = $("input:checkbox[name=filterLabel]:checked").map(function(){ 366 | return $(this).val(); 367 | }).toArray(); 368 | 369 | console.log('selected filters:', selectedFilters); 370 | var filter = selectedFilters.join(','); 371 | 372 | var url = '/api/videos'; 373 | if (filter) 374 | url += "?filter=" + filter + '&unassigned=' + $scope.btnAllOrUnassigned; 375 | else 376 | url += '?unassigned=' + $scope.btnAllOrUnassigned; 377 | 378 | $scope.ajaxStart(); 379 | $http({ method: 'GET', url: url }) 380 | .success(function (result) { 381 | $scope.videos = result.videos; 382 | angular.forEach($scope.videos, function (video) { 383 | video.Labels = video.Labels && video.Labels.split(','); 384 | }) 385 | $scope.ajaxCompleted(); 386 | }); 387 | } 388 | 389 | $scope.filterFetch = function(mode) { 390 | $scope.btnAllOrUnassigned = mode; 391 | getVideos(); 392 | } 393 | 394 | $scope.addVideo = function () { 395 | $location.path('/videos/0'); 396 | } 397 | 398 | $scope.editVideo = function () { 399 | var videoId = this.video.Id; 400 | $location.path('/videos/' + videoId); 401 | } 402 | 403 | $scope.createJob = function() { 404 | var videoId = this.video.Id; 405 | console.log('creating job for video id', videoId); 406 | $location.path('/jobs/0').search({'videoId': videoId }); 407 | } 408 | 409 | $scope.download = function () { 410 | var videoId = this.video.Id; 411 | var url = '/api/videos/' + videoId + '/frames'; 412 | window.open(url, '_blank'); 413 | } 414 | 415 | $scope.filter = function() { 416 | getVideos(); 417 | } 418 | }]) 419 | 420 | .controller('TagJobController', ['$scope', '$route', '$http', '$location', '$routeParams', '$timeout', 'state', function ($scope, $route, $http, $location, $routeParams, $timeout, state) { 421 | 422 | $scope.clearMessages(); 423 | var videoCtrl = document.getElementById('video-tagging'); 424 | var jobId = $routeParams.id; 425 | 426 | $scope.ajaxStart(); 427 | $http({ method: 'GET', url: '/api/jobs/' + jobId }) 428 | .success(function (jobData) { 429 | $scope.jobData = jobData; 430 | console.log('jobData', jobData); 431 | 432 | videoCtrl.videoduration = jobData.video.DurationSeconds; 433 | videoCtrl.videowidth = jobData.video.Width; 434 | videoCtrl.videoheight = jobData.video.Height; 435 | videoCtrl.framerate = jobData.video.FramesPerSecond; 436 | 437 | videoCtrl.regiontype = jobData.job.Config.regiontype; 438 | videoCtrl.multiregions = jobData.job.Config.multiregions; 439 | videoCtrl.regionsize = jobData.job.Config.regionsize; 440 | 441 | videoCtrl.inputtagsarray = jobData.job.Config.tags; 442 | 443 | $http({ method: 'GET', url: '/api/jobs/' + $routeParams.id + '/frames' }) 444 | .success(function (result) { 445 | videoCtrl.inputframes = result.frames; 446 | console.log(videoCtrl.inputframes) 447 | videoCtrl.src = ''; 448 | console.log('video url', jobData.video.Url); 449 | videoCtrl.src = jobData.video.Url; 450 | $scope.ajaxCompleted(); 451 | }); 452 | }); 453 | 454 | $scope.updateJobStatus = function (status) { 455 | $scope.clearMessages(); 456 | var statusId = state.getJobStatusByName()[status]; 457 | var data = { statusId: statusId }; 458 | 459 | $scope.ajaxStart(); 460 | $http({ method: 'POST', url: "/api/jobs/" + jobId + "/status", data: data }) 461 | .success(function () { 462 | console.log('success'); 463 | $scope.showInfo('job updated'); 464 | $scope.ajaxCompleted(); 465 | }) 466 | .error(function (err) { 467 | console.error(err); 468 | $scope.showError('error updating job: ' + err.message); 469 | $scope.ajaxCompleted(); 470 | }); 471 | } 472 | 473 | function tagHandler(e) { 474 | console.log('handler called'); 475 | var inputObject = e.detail.frame; 476 | var msg = {}; 477 | msg.tags = inputObject.regions; 478 | $http({ method: 'POST', url: '/api/jobs/' + jobId + '/frames/' + inputObject.frameIndex, data: msg }) 479 | .success(function (result) { 480 | console.log('frame saved successfully'); 481 | $scope.clearMessages(); 482 | }) 483 | .error(function (err) { 484 | console.error('error', err); 485 | $scope.showError('error saving frame: ' + err.message || ''); 486 | }); 487 | } 488 | 489 | videoCtrl.addEventListener('onregionchanged', tagHandler); 490 | 491 | $scope.$on('$destroy', function() { 492 | videoCtrl.removeEventListener('onregionchanged', tagHandler); 493 | }) 494 | }]) 495 | 496 | .controller('UpsertVideoController', ['$scope', '$http', '$location', '$routeParams', function ($scope, $http, $location, $routeParams) { 497 | 498 | var defaultId = -1; 499 | 500 | $scope.videoId = defaultId; 501 | $scope.config = '{}'; 502 | $scope.progress = null; 503 | 504 | function getLabels(cb) { 505 | var labels = []; 506 | $http({ method: 'GET', url: '/api/labels' }) 507 | .success(function (result) { 508 | angular.forEach( 509 | result.labels, 510 | function(label) { 511 | label.value = label.Name; 512 | labels.push(label); 513 | }); 514 | return cb(labels); 515 | }); 516 | } 517 | 518 | function initTokenField(labels) { 519 | $('#tokenfield').tokenfield({ 520 | autocomplete: { 521 | source: labels, 522 | delay: 100 523 | }, 524 | showAutocompleteOnFocus: true 525 | }); 526 | } 527 | 528 | 529 | if ($routeParams.id != 0) { 530 | $scope.ajaxStart(); 531 | $http({ method: 'GET', url: '/api/videos/' + $routeParams.id }) 532 | .success(function (video) { 533 | console.log('video', video); 534 | 535 | $scope.videoId = video.Id; 536 | $scope.name = video.Name; 537 | $scope.url = video.Url; 538 | $scope.height = video.Height; 539 | $scope.width = video.Width; 540 | $scope.duration = video.DurationSeconds.toFixed(2); 541 | $scope.framesPerSecond = video.FramesPerSecond.toFixed(2); 542 | $scope.videoUploaded = video.VideoUploaded; 543 | $scope.videoLabels = video.Labels; 544 | 545 | getLabels(function(labels){ 546 | initTokenField(labels); 547 | }); 548 | 549 | $scope.ajaxCompleted(); 550 | }); 551 | } 552 | else { 553 | getLabels(function(labels){ 554 | initTokenField(labels); 555 | }); 556 | } 557 | 558 | $scope.submit = function () { 559 | 560 | $scope.clearMessages(); 561 | 562 | if(!$scope.name) return $scope.showError('name was not provided'); 563 | if(!$scope.height) return $scope.showError('height was not provided'); 564 | if(!$scope.width) return $scope.showError('width was not provided'); 565 | if(!$scope.duration) return $scope.showError('duration was not provided'); 566 | if(!$scope.framesPerSecond) return $scope.showError('framesPerSecond was not provided'); 567 | 568 | var labels = $('#tokenfield').tokenfield('getTokens').map(function(label) {return label.value;}); 569 | 570 | // First add the video to the database 571 | var data = { 572 | name: $scope.name, 573 | height: $scope.height, 574 | width: $scope.width, 575 | durationSeconds: $scope.duration, 576 | framesPerSecond: $scope.framesPerSecond, 577 | labels: labels 578 | }; 579 | 580 | if ($scope.videoId != defaultId) { 581 | data.id = $scope.videoId; 582 | } 583 | 584 | console.log('submitting', data); 585 | 586 | $scope.ajaxStart(); 587 | $http({ method: 'POST', url: '/api/videos', data: data }) 588 | .success(function (result) { 589 | console.log('result', result); 590 | $scope.showInfo('video ' + result.videoId + ($scope.videoId == defaultId ? ' created' : ' modified') + ' successfully'); 591 | $scope.videoId = result.videoId; 592 | $scope.ajaxCompleted(); 593 | }) 594 | .error(function (err) { 595 | console.error(err); 596 | $scope.showError(err.message); 597 | $scope.ajaxCompleted(); 598 | }); 599 | } 600 | 601 | 602 | $scope.submitVideo = function () { 603 | $scope.clearMessages(); 604 | var file = document.getElementById('inputFile').files[0]; 605 | 606 | $.get('/api/videos/' + $scope.videoId + '/url') 607 | .success(function (result) { 608 | console.log('url', result); 609 | uploadImage(result.url); 610 | }) 611 | .error(function (err) { 612 | console.error('error getting blob Url', err); 613 | }); 614 | 615 | function uploadImage(url) { 616 | 617 | var maxBlockSize = 256 * 1024; //Each file will be split in 256 KB. 618 | var numberOfBlocks = 1; 619 | var selectedFile = file; 620 | var currentFilePointer = 0; 621 | var totalBytesRemaining = 0; 622 | var blockIds = new Array(); 623 | var blockIdPrefix = "block-"; 624 | var submitUri = url; 625 | var bytesUploaded = 0; 626 | 627 | var fileSize = selectedFile.size; 628 | if (fileSize < maxBlockSize) { 629 | maxBlockSize = fileSize; 630 | console.log("max block size = " + maxBlockSize); 631 | } 632 | totalBytesRemaining = fileSize; 633 | if (fileSize % maxBlockSize == 0) { 634 | numberOfBlocks = fileSize / maxBlockSize; 635 | } else { 636 | numberOfBlocks = parseInt(fileSize / maxBlockSize, 10) + 1; 637 | } 638 | 639 | console.log("total blocks = " + numberOfBlocks); 640 | console.log(submitUri); 641 | 642 | var reader = new FileReader(); 643 | 644 | reader.onloadend = function (evt) { 645 | if (evt.target.readyState == FileReader.DONE) { // DONE == 2 646 | var uri = submitUri + '&comp=block&blockid=' + blockIds[blockIds.length - 1]; 647 | var requestData = new Uint8Array(evt.target.result); 648 | $.ajax({ 649 | url: uri, 650 | type: "PUT", 651 | data: requestData, 652 | processData: false, 653 | beforeSend: function (xhr) { 654 | xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob'); 655 | }, 656 | success: function (data, status) { 657 | console.log(data); 658 | console.log(status); 659 | bytesUploaded += requestData.length; 660 | var percentComplete = ((parseFloat(bytesUploaded) / parseFloat(selectedFile.size)) * 100).toFixed(2); 661 | updateProgress(percentComplete); 662 | console.log(percentComplete + " %"); 663 | uploadFileInBlocks(); 664 | }, 665 | error: function (xhr, desc, err) { 666 | console.log(desc); 667 | console.log(err); 668 | } 669 | }); 670 | } 671 | }; 672 | 673 | uploadFileInBlocks(); 674 | function uploadFileInBlocks() { 675 | if (totalBytesRemaining > 0) { 676 | console.log("current file pointer = " + currentFilePointer + " bytes read = " + maxBlockSize); 677 | var fileContent = selectedFile.slice(currentFilePointer, currentFilePointer + maxBlockSize); 678 | var blockId = blockIdPrefix + pad(blockIds.length, 6); 679 | console.log("block id = " + blockId); 680 | blockIds.push(btoa(blockId)); 681 | reader.readAsArrayBuffer(fileContent); 682 | currentFilePointer += maxBlockSize; 683 | totalBytesRemaining -= maxBlockSize; 684 | if (totalBytesRemaining < maxBlockSize) { 685 | maxBlockSize = totalBytesRemaining; 686 | } 687 | } else { 688 | commitBlockList(); 689 | } 690 | } 691 | 692 | function commitBlockList() { 693 | var uri = submitUri + '&comp=blocklist'; 694 | console.log(uri); 695 | var requestBody = ''; 696 | for (var i = 0; i < blockIds.length; i++) { 697 | requestBody += '' + blockIds[i] + ''; 698 | } 699 | requestBody += ''; 700 | console.log(requestBody); 701 | $.ajax({ 702 | url: uri, 703 | type: "PUT", 704 | data: requestBody, 705 | beforeSend: function (xhr) { 706 | xhr.setRequestHeader('x-ms-blob-content-type', selectedFile.type); 707 | }, 708 | success: function (data, status) { 709 | console.log(data); 710 | console.log(status); 711 | updateVideoAsLoaded(); 712 | }, 713 | error: function (xhr, desc, err) { 714 | console.log(desc); 715 | console.log(err); 716 | } 717 | }); 718 | } 719 | function pad(number, length) { 720 | var str = '' + number; 721 | while (str.length < length) { 722 | str = '0' + str; 723 | } 724 | return str; 725 | } 726 | 727 | function updateProgress(progress) { 728 | $scope.$apply(function () { 729 | console.log('got progress', progress); 730 | 731 | $scope.progress = progress; 732 | if (progress == 100) { 733 | setTimeout(function () { 734 | $scope.$apply(function () { 735 | $scope.progress = null; 736 | }); 737 | }, 3000); 738 | } 739 | }); 740 | } 741 | 742 | function updateVideoAsLoaded() { 743 | $.ajax({ 744 | url: '/api/videos/' + $scope.videoId, 745 | type: "POST", 746 | success: function (data, status) { 747 | console.log('upload success!!!') 748 | }, 749 | error: function (xhr, desc, err) { 750 | console.log(desc); 751 | console.log(err); 752 | } 753 | }); 754 | } 755 | } 756 | } 757 | }]) 758 | 759 | .controller('UsersController', ['$scope', '$route', '$http', '$location', '$routeParams', function ($scope, $route, $http, $location, $routeParams) { 760 | var users = []; 761 | 762 | $scope.ajaxStart(); 763 | $http({ method: 'GET', url: '/api/users' }) 764 | .success(function (result) { 765 | users = $scope.users = result.users; 766 | $scope.ajaxCompleted(); 767 | }); 768 | 769 | $scope.editUser = function () { 770 | $location.path('/users/' + this.user.Id); 771 | } 772 | 773 | $scope.addUser = function () { 774 | $location.path('/users/0'); 775 | } 776 | 777 | }]); -------------------------------------------------------------------------------- /public/partials/about.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 | 5 |

about

6 | 7 |

8 | Read about this tool here 9 |

10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /public/partials/contact.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 | 5 |

contact

6 | 7 | TODO: fill in your contact details here 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /public/partials/jobs.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |

jobs

5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 65 | 66 |
actioniddescriptionstatuscreation datecreated byownerapprovedvideosettings
41 | 42 | 43 | {{ job.JobId }}{{ job.Description }}{{ job.StatusName }}{{ job.CreateDate | date:'dd-MM-yyyy' }}{{ job.CreatedByName }}{{ job.UserName }}{{ job.ReviewedByName }}{{ job.VideoName }} 54 | 55 | 56 | 57 | 58 | 59 | 60 |
region type{{ job.Config.regiontype }}
multi-regions{{ job.Config.multiregions ? 'true' : 'false'}}
region size{{ job.Config.regionsize }}
tags{{ job.Config && job.Config.tags && job.Config.tags.join(', ') }}
61 | 62 |
67 |
68 | 69 | 70 | 71 |
72 | -------------------------------------------------------------------------------- /public/partials/tagJob.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |

{{ jobData.job.Description }}

5 |
Id: {{jobData.job.Id}}, Created {{jobData.job.CreateDate | date:'dd-MM-yyyy'}}
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /public/partials/terms.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 | 5 |

terms of use

6 | 7 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 8 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 9 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 10 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 11 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 12 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 13 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 14 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 15 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 16 | terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... terms of use... 17 |
18 |
19 | -------------------------------------------------------------------------------- /public/partials/upsertUser.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |

5 |
6 | 7 |
8 | 9 |
10 | {{userId}} 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 |
60 | -------------------------------------------------------------------------------- /public/partials/upsertVideo.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |

5 |
6 | 7 |
8 | 9 | 10 |
11 | {{videoId}} 12 | preview 13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 | HINT: Type ',' after a new tag to add it 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 |
75 | 76 | {{ progress }}% 77 |
78 |
79 | 80 |
81 |
82 | 83 | 84 |
85 | 86 |
87 | 88 |
89 | 90 |
91 |
92 | 93 |
94 | -------------------------------------------------------------------------------- /public/partials/upsertjob.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |

{{ description }}

5 |
6 | 7 |
8 | 9 |
10 | {{jobId}} 11 |
12 |
13 | 14 |
15 | 16 |
17 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | 38 | 42 |
43 |
44 | 45 | 46 |
47 | 48 |
49 | 50 |
51 |
52 | 53 | 54 |
55 | 56 |
57 |
58 | 59 |
60 | 64 |
65 |
66 |
67 | 68 |
69 | 74 |
75 |
76 |
77 | 78 |
79 | 84 |
85 |
86 |
87 | 88 |
89 | 90 |
91 |
92 |
93 |
94 | 95 |
96 |
97 | 98 |
99 |
100 | 101 |
102 | 103 |
104 | 105 |
106 | 107 |
108 | 109 | 110 |
111 | 112 |
113 | 114 | 115 |
116 | -------------------------------------------------------------------------------- /public/partials/users.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |

users

5 | 6 |
7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
actionidnameemailrole
25 | {{ user.Id }}{{ user.Name }}{{ user.Email }}{{ user.RoleName }}
33 |
34 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /public/partials/videos.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 |

videos

5 | 6 |
7 |
8 | 9 |
10 | 11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 |
actionidtitlewidthheightduration (secs)FPSlabelsdownload
48 | 49 | 50 | {{ video.Id }} 53 | {{ video.Name }} 54 | {{ video.Name }} 55 | {{ video.Width }}{{ video.Height }}{{ video.DurationSeconds.toFixed(2)}}{{ video.FramesPerSecond.toFixed(2) }} 61 | 62 | {{ label }} 63 | 64 |
70 |
71 |
72 |
73 | 74 | 75 |
76 | -------------------------------------------------------------------------------- /public/partials/welcome.html: -------------------------------------------------------------------------------- 1 |  2 |
3 |
4 | 5 |
6 |

welcome, {{user.Name}}

7 | check out your jobs 8 |
9 |
please login
10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var multiparty = require('multiparty'); 4 | var path = require('path'); 5 | var db = require('../storage/db'); 6 | var blob = require('../storage/blob'); 7 | 8 | module.exports = function () { 9 | 10 | router.post('/jobs', AdminLoggedIn, function (req, res) { 11 | req.body.createdById = req.user.Id; 12 | db.createOrModifyJob(req.body, function (err, result) { 13 | if (err) return logError(err, res); 14 | res.json(result); 15 | }); 16 | }); 17 | 18 | router.post('/users', AdminLoggedIn, function (req, res) { 19 | db.createOrModifyUser(req.body, function (err, result) { 20 | if (err) return logError(err, res); 21 | res.json(result); 22 | }); 23 | }); 24 | 25 | router.post('/videos', AdminLoggedIn, function (req, res) { 26 | db.createOrModifyVideo(req.body, function (err, result) { 27 | if (err) return logError(err, res); 28 | res.json(result); 29 | }); 30 | }); 31 | 32 | router.post('/users', AdminLoggedIn, function (req, res) { 33 | db.createOrModifyUser(req.body, function (err, result) { 34 | if (err) return logError(err, res); 35 | res.json(result); 36 | }); 37 | }); 38 | 39 | router.get('/videos/:id/url', AdminLoggedIn, function (req, res) { 40 | var id = req.params.id; 41 | console.log('getting url for blob', id); 42 | var url = blob.getVideoUrlWithSasWrite(id); 43 | return res.json({ url: url }); 44 | }); 45 | 46 | router.post('/videos/:id', AdminLoggedIn, function (req, res) { 47 | var id = req.params.id; 48 | console.log('video uploaded', id); 49 | 50 | db.updateVideoUploaded({id: id}, function(err) { 51 | if (err) return logError(err, res); 52 | return res.json({ status: "OK" }); 53 | }); 54 | }); 55 | 56 | // TODO: check job belong to editor / Admin mode, if Approved check user is Admin 57 | router.post('/jobs/:id/status', EditorLoggedIn, function (req, res) { 58 | var id = req.body.id = req.params.id; 59 | req.body.userId = req.user.Id; 60 | console.log('updating status for job', id); 61 | db.updateJobStatus(req.body, function (err, resp) { 62 | if (err) return logError(err, res); 63 | res.json(resp); 64 | }); 65 | }); 66 | 67 | router.get('/jobs/statuses', function (req, res) { 68 | console.log('getting jobs statuses'); 69 | db.getJobstatuses(function (err, resp) { 70 | if (err) return logError(err, res); 71 | res.json(resp); 72 | }); 73 | }); 74 | 75 | router.get('/roles', function (req, res) { 76 | console.log('getting roles'); 77 | db.getRoles(function (err, resp) { 78 | if (err) return res.status(500).json({ error: err }); 79 | res.json(resp); 80 | }); 81 | }); 82 | 83 | // TODO: check job belong to editor / Admin mode 84 | router.get('/jobs/:id/frames', EditorLoggedIn, function (req, res) { 85 | var id = req.params.id; 86 | console.log('getting frames for job', id); 87 | db.getVideoFramesByJob(id, function (err, resp) { 88 | if (err) return logError(err, res); 89 | res.json(resp); 90 | }); 91 | }); 92 | 93 | // TODO: check job belong to editor / Admin mode 94 | router.get('/jobs/:id', EditorLoggedIn, function (req, res) { 95 | var id = req.params.id; 96 | console.log('getting job id', id); 97 | db.getJobDetails(id, function (err, resp) { 98 | if (err) return logError(err, res); 99 | res.json(resp); 100 | }); 101 | }); 102 | 103 | router.get('/jobs', AdminLoggedIn, function (req, res) { 104 | console.log('getting all jobs'); 105 | db.getAllJobs(function (err, resp) { 106 | if (err) return logError(err, res); 107 | res.json(resp); 108 | }); 109 | }); 110 | 111 | // TODO: check job belong to editor / Admin mode 112 | router.post('/jobs/:id/frames/:index', EditorLoggedIn, function (req, res) { 113 | var options = { 114 | tagsJson: req.body.tags 115 | }; 116 | options.jobId = req.params.id; 117 | options.frameIndex = req.params.index; 118 | 119 | console.log('posing frame index', options.frameIndex, 'for job', options.jobId); 120 | db.createOrModifyFrame(options, function (err) { 121 | if (err) return logError(err, res); 122 | res.json({}); 123 | }); 124 | }); 125 | 126 | router.get('/users/:id/jobs', [EditorLoggedIn, AuthorizeUserAction], function (req, res) { 127 | var userId = req.user.Id; 128 | console.log('getting jobs for user id', userId); 129 | db.getUserJobs(userId, function (err, resp) { 130 | if (err) return logError(err, res); 131 | res.json(resp); 132 | }); 133 | }); 134 | 135 | router.get('/videos', AdminLoggedIn, function (req, res) { 136 | var labels = []; 137 | var filter = req.query.filter; 138 | var unassigned = req.query.unassigned == '1' ? 1 : 0; 139 | if (filter) { 140 | var labels = filter.split(','); 141 | console.log('getting videos labeled ' + labels); 142 | } 143 | 144 | return db.getVideos({ 145 | labels: labels, 146 | unassigned: unassigned 147 | }, 148 | function (err, resp) { 149 | if (err) return res.status(500).json({ error: err }); 150 | console.log('resp:', resp); 151 | res.json(resp); 152 | }); 153 | }); 154 | 155 | router.get('/videos/:id', EditorLoggedIn, function (req, res) { 156 | var id = req.params.id; 157 | console.log('getting video', id); 158 | db.getVideo(id, function (err, resp) { 159 | if (err) return logError(err, res); 160 | res.json(resp); 161 | }); 162 | }); 163 | 164 | // Not in use- can be used to stream the movie through the API if we 165 | // want to force authentication & authorization 166 | router.get('/videos/:id/movie', EditorLoggedIn, function (req, res) { 167 | var id = req.params.id; 168 | console.log('getting video file', id); 169 | 170 | return blob.getVideoStream({ name: id }, function (err, result) { 171 | if (err) return logError(err, res); 172 | 173 | console.log('stream', result); 174 | 175 | res.setHeader('content-type', result.contentType); 176 | res.setHeader('content-length', result.contentLength); 177 | res.setHeader('etag', result.etag); 178 | 179 | result.stream.on('error', function (err) { 180 | console.error(err); 181 | return res.status(500).json({ message: err.message }); 182 | }); 183 | 184 | result.stream.pipe(res); 185 | }); 186 | }); 187 | 188 | router.get('/users/:id', AdminLoggedIn, function (req, res) { 189 | var id = req.params.id; 190 | console.log('getting user', id); 191 | db.getUserById(id, function (err, resp) { 192 | if (err) return logError(err, res); 193 | res.json(resp); 194 | }); 195 | }); 196 | 197 | router.get('/users', AdminLoggedIn, function (req, res) { 198 | console.log('getting users'); 199 | db.getUsers(function (err, resp) { 200 | if (err) return logError(err, res); 201 | res.json(resp); 202 | }); 203 | }); 204 | 205 | router.get('/videos/:id/frames', EditorLoggedIn, function (req, res) { 206 | var id = req.params.id; 207 | console.log('getting frames for video', id); 208 | db.getVideo(id, function (err, video) { 209 | if (err) return logError(err, res); 210 | db.getVideoFrames(id, function (err, resp) { 211 | if (err) return logError(err, res); 212 | res.setHeader("Content-Type", "application/json"); 213 | res.setHeader("Content-Disposition", "attachment;filename=" + video.Name + ".tags.json"); 214 | res.setHeader("Content-Transfer-Encoding", "utf-8"); 215 | res.json(resp); 216 | }); 217 | }); 218 | }); 219 | 220 | router.get('/labels', AdminLoggedIn, function (req, res) { 221 | console.log('getting all labels'); 222 | db.getLabels(function (err, resp) { 223 | if (err) return res.status(500).json({ error: err }); 224 | res.json(resp); 225 | }); 226 | }); 227 | 228 | return router; 229 | } 230 | 231 | function logError(err, res) { 232 | console.error('error:', err); 233 | return res.status(500).json({ message: err.message }); 234 | } 235 | 236 | var AdminLoggedIn = getLoggedInForRole(['Admin']); 237 | var EditorLoggedIn = getLoggedInForRole(['Admin', 'Editor']); 238 | 239 | function AuthorizeUserAction(req, res, next) { 240 | var id = req.params.id; 241 | if (id && req.user.RoleName === 'Editor' && req.user.Id != id) { 242 | return res.status(401).json({ error: 'user is not an Admin, can\'t access other user data' }); 243 | } 244 | return next(); 245 | } 246 | 247 | function getLoggedInForRole(roles) { 248 | return function(req, res, next) { 249 | 250 | // if user is authenticated in the session, and in role 251 | if (!req.isAuthenticated()) 252 | return res.status(401).json({ error: 'user not logged in' }); 253 | 254 | var found = false; 255 | for (var i = 0; i < roles.length; i++) 256 | if (req.user.RoleName === roles[i]) { 257 | found = true; 258 | break; 259 | } 260 | 261 | if (!found) 262 | return res.status(401).json({ error: 'user not in ' + JSON.stringify(roles) + ' role' }); 263 | 264 | return next(); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /routes/login.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | module.exports = function (passport) { 5 | 6 | router.get('/login', passport.authenticate('google', { scope : ['https://www.googleapis.com/auth/plus.profile.emails.read'] })); 7 | 8 | // the callback after google has authenticated the user 9 | router.get('/.auth/login/google/callback', 10 | passport.authenticate('google', { 11 | successRedirect : '/#/jobs', 12 | failureRedirect : '/Login' 13 | })); 14 | 15 | router.get('/logout', function (req, res) { 16 | req.logout(); 17 | res.redirect('/'); 18 | }); 19 | 20 | router.get('/profile', isLoggedIn, function (req, res) { 21 | res.json(req.user); 22 | }); 23 | 24 | return router; 25 | }; 26 | 27 | // route middleware to make sure a user is logged in 28 | function isLoggedIn(req, res, next) { 29 | 30 | // if user is authenticated in the session, carry on 31 | if (req.isAuthenticated()) 32 | return next(); 33 | 34 | // if they aren't redirect them to the home page 35 | //res.redirect('/'); 36 | return res.status(401).json({ error: 'user not logged in' }); 37 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 |  2 | var express = require('express'); 3 | var path = require('path'); 4 | var bodyParser = require('body-parser'); 5 | var api = require('./routes/api'); 6 | 7 | var passport = require('passport'); 8 | var flash = require('connect-flash'); 9 | var morgan = require('morgan'); 10 | var cookieParser = require('cookie-parser'); 11 | var bodyParser = require('body-parser'); 12 | var session = require('express-session'); 13 | 14 | require('./auth/passport')(passport); 15 | 16 | var app = express(); 17 | 18 | app.use(morgan('dev')); // log every request to the console 19 | 20 | app.use(cookieParser()); // read cookies (needed for auth) 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | 24 | 25 | // required for passport 26 | app.use(session({ secret: 'mysecretsesson123456789' })); // session secret 27 | app.use(passport.initialize()); 28 | app.use(passport.session()); // persistent login sessions 29 | app.use(flash()); // use connect-flash for flash messages stored in session 30 | 31 | app.use(require('./routes/login')(passport)); 32 | app.use('/api', api()); 33 | 34 | app.use(express.static(path.join(__dirname, 'public'))); 35 | 36 | app.use(function (req, res) { 37 | return res.status(404).json({ error: 'not found' }); 38 | }); 39 | 40 | app.set('port', process.env.PORT || 3000); 41 | 42 | var server = app.listen(app.get('port'), function() { 43 | console.log('Express server listening on port ' + server.address().port); 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /storage/blob.js: -------------------------------------------------------------------------------- 1 | var azure = require('azure-storage'); 2 | var config = require('../config'); 3 | var url = require('url'); 4 | 5 | var CONTAINER_NAME = 'vidoes'; 6 | var URL_FORMAT = 'https://.blob.core.windows.net/' 7 | .replace('', config.storage.account) 8 | .replace('', CONTAINER_NAME); 9 | 10 | var blobSvc = azure.createBlobService(config.storage.account, config.storage.key); 11 | 12 | var cbUrl = config.auth.google.callbackURL, 13 | cbUrlElements = url.parse(cbUrl), 14 | host = cbUrlElements.protocol + '//' + cbUrlElements.host; 15 | 16 | console.log('enabling blob CORS for host', host); 17 | var serviceProperties = { 18 | Cors: { 19 | CorsRule: [{ 20 | AllowedOrigins: [host], 21 | AllowedMethods: ['GET', 'PUT'], 22 | AllowedHeaders: ['*'], 23 | ExposedHeaders: ['*'], 24 | MaxAgeInSeconds: 30 * 60 25 | }] 26 | } 27 | }; 28 | 29 | blobSvc.setServiceProperties(serviceProperties, function (err, result) { 30 | if (err) return console.error('error setting service properties', err); 31 | console.log('result:', result); 32 | 33 | blobSvc.createContainerIfNotExists(CONTAINER_NAME, { publicAccessLevel : 'blob' }, 34 | function (err, result, response) { 35 | if (err) return console.error('error creating container', CONTAINER_NAME, err); 36 | }); 37 | }); 38 | 39 | function upload(opts, cb) { 40 | return blobSvc.createBlockBlobFromStream(CONTAINER_NAME, opts.name, opts.stream, opts.size, 41 | { contentType: opts.contentType }, 42 | function (err, file, result) { 43 | if (err) { 44 | console.error('error saving blob', opts, err); 45 | return cb(err); 46 | } 47 | return cb(null, { url: URL_FORMAT + '/' + opts.name }); 48 | }); 49 | } 50 | 51 | function getSAS(opts) { 52 | 53 | var permissions = opts.permissions || azure.BlobUtilities.SharedAccessPermissions.READ; 54 | var startDate = new Date(); 55 | var expiryDate = new Date(startDate); 56 | expiryDate.setMinutes(startDate.getMinutes() + 30); 57 | startDate.setMinutes(startDate.getMinutes() - 10); 58 | 59 | var sharedAccessPolicy = { 60 | AccessPolicy: { 61 | Permissions: permissions, 62 | Start: startDate, 63 | Expiry: expiryDate 64 | } 65 | }; 66 | var sasToken = blobSvc.generateSharedAccessSignature(CONTAINER_NAME, opts.name + '', sharedAccessPolicy); 67 | console.log('sasToken', sasToken); 68 | return sasToken; 69 | } 70 | 71 | function getVideoStream(opts, cb) { 72 | return blobSvc.getBlobProperties(CONTAINER_NAME, opts.name, function(err, props){ 73 | if (err) return cb(err); 74 | var stream = blobSvc.createReadStream(CONTAINER_NAME, opts.name); 75 | return cb(null, { 76 | stream: stream, 77 | contentType: props.contentType, 78 | contentLength: props.contentLength, 79 | etag: props.etag 80 | }); 81 | 82 | }); 83 | } 84 | 85 | function getVideoUrl(id) { 86 | return URL_FORMAT + '/' + id; 87 | } 88 | 89 | function getVideoUrlWithSas(id) { 90 | return getVideoUrl(id) + '?' + getSAS({ name: id }); 91 | } 92 | 93 | function getVideoUrlWithSasWrite(id) { 94 | return getVideoUrl(id) + '?' + getSAS({ name: id, 95 | permissions: azure.BlobUtilities.SharedAccessPermissions.WRITE}); 96 | } 97 | 98 | module.exports = { 99 | upload: upload, 100 | getVideoStream: getVideoStream, 101 | getVideoUrlWithSas: getVideoUrlWithSas, 102 | getVideoUrlWithSasWrite: getVideoUrlWithSasWrite 103 | }; 104 | 105 | 106 | -------------------------------------------------------------------------------- /storage/db.js: -------------------------------------------------------------------------------- 1 | var tedious = require('tedious'); 2 | var TYPES = tedious.TYPES; 3 | var configSql = require('../config').sql; 4 | 5 | var DBErrors = { 6 | duplicate: 2601 7 | } 8 | 9 | var blob = require('./blob'); 10 | 11 | function connect(cb) { 12 | console.log('connecting to server', configSql.server); 13 | 14 | var Connection = tedious.Connection; 15 | var connection = new Connection(configSql); 16 | 17 | connection.on('connect', function(err) { 18 | if (err) { 19 | console.error('error connecting to sql server', configSql.server); 20 | return cb(err); 21 | } 22 | console.log('connection established', !connection.closed); 23 | return cb(null, connection); 24 | }); 25 | } 26 | 27 | function normalizeVideoRow(video) { 28 | if(video.VideoJson) 29 | video.Data = JSON.parse(video.VideoJson); 30 | delete video.VideoJson; 31 | 32 | video.Url = blob.getVideoUrlWithSas(video.Id); 33 | return video; 34 | } 35 | 36 | function normalizeFrameRow(frame) { 37 | if(frame.TagsJson) 38 | frame.Tags = JSON.parse(frame.TagsJson); 39 | delete frame.TagsJson; 40 | return frame; 41 | } 42 | 43 | function normalizeJobRow(job) { 44 | if(job.ConfigJson) 45 | job.Config = JSON.parse(job.ConfigJson); 46 | delete job.ConfigJson; 47 | return job; 48 | } 49 | 50 | function getJobDetails(id, cb) { 51 | return getDataSets({ 52 | sproc: 'GetJob', 53 | sets: ['job', 'video', 'user', 'frames'], 54 | params: [{name: 'Id', type: TYPES.Int, value: id}] 55 | }, function(err, result){ 56 | if (err) return logError(err, cb); 57 | 58 | var newResult = { 59 | job: result.job[0], 60 | video: result.video[0], 61 | user: result.user[0] 62 | }; 63 | 64 | try { 65 | normalizeJobRow(newResult.job); 66 | normalizeVideoRow(newResult.video); 67 | } 68 | catch (err) { 69 | return logError(err, cb); 70 | } 71 | 72 | return cb(null, newResult); 73 | }); 74 | } 75 | 76 | function getUsers(cb) { 77 | return getDataSets({ 78 | sproc: 'GetUsers', 79 | sets: ['users'], 80 | params: [] 81 | }, function (err, result) { 82 | if (err) return logError(err, cb); 83 | 84 | var newResult = { 85 | users: [] 86 | }; 87 | 88 | try { 89 | for (var i = 0; i < result.users.length; i++) { 90 | newResult.users.push(result.users[i]); 91 | } 92 | } 93 | catch (err) { 94 | return logError(err, cb); 95 | } 96 | 97 | return cb(null, newResult); 98 | }); 99 | } 100 | 101 | function createOrModifyUser(req, cb) { 102 | connect(function (err, connection) { 103 | if (err) return cb(err); 104 | 105 | try { 106 | var resultUserId; 107 | 108 | var request = new tedious.Request('UpsertUser', function (err) { 109 | if (err) return logError(err, cb); 110 | return cb(null, { userId: resultUserId }); 111 | }); 112 | 113 | if (req.id) 114 | request.addParameter('Id', TYPES.Int, req.id); 115 | 116 | request.addParameter('Name', TYPES.NVarChar, req.name); 117 | request.addParameter('Email', TYPES.NVarChar, req.email); 118 | request.addParameter('RoleId', TYPES.TinyInt, req.roleId); 119 | 120 | request.addOutputParameter('UserId', TYPES.Int); 121 | 122 | request.on('returnValue', function (parameterName, value, metadata) { 123 | if (parameterName == 'UserId') { 124 | resultUserId = value; 125 | } 126 | }); 127 | 128 | connection.callProcedure(request); 129 | } 130 | catch (err) { 131 | return cb(err); 132 | } 133 | 134 | }); 135 | } 136 | 137 | function updateJobStatus(req, cb) { 138 | connect(function (err, connection) { 139 | if (err) return cb(err); 140 | 141 | try { 142 | 143 | var request = new tedious.Request('UpdateJobStatus', function (err) { 144 | if (err) return logError(err, cb); 145 | return cb(); 146 | }); 147 | 148 | request.addParameter('Id', TYPES.Int, req.id); 149 | request.addParameter('UserId', TYPES.Int, req.userId); 150 | request.addParameter('StatusId', TYPES.TinyInt, req.statusId); 151 | 152 | connection.callProcedure(request); 153 | } 154 | catch (err) { 155 | return cb(err); 156 | } 157 | 158 | }); 159 | } 160 | 161 | function updateVideoUploaded(req, cb) { 162 | connect(function (err, connection) { 163 | if (err) return cb(err); 164 | 165 | try { 166 | 167 | var request = new tedious.Request('UpdateVideoUploaded', function (err) { 168 | if (err) return logError(err, cb); 169 | return cb(); 170 | }); 171 | 172 | request.addParameter('Id', TYPES.Int, req.id); 173 | connection.callProcedure(request); 174 | } 175 | catch (err) { 176 | return cb(err); 177 | } 178 | 179 | }); 180 | } 181 | 182 | function getJobstatuses(cb) { 183 | return getDataSets({ 184 | sproc: 'GetJobStatuses', 185 | sets: ['statuses'], 186 | params: [] 187 | }, function (err, result) { 188 | if (err) return logError(err, cb); 189 | return cb(null, result); 190 | }); 191 | } 192 | 193 | function getRoles(cb) { 194 | return getDataSets({ 195 | sproc: 'GetRoles', 196 | sets: ['roles'], 197 | params: [] 198 | }, function (err, result) { 199 | if (err) return logError(err, cb); 200 | return cb(null, result); 201 | }); 202 | } 203 | 204 | function getVideo(id, cb) { 205 | return getDataSets({ 206 | sproc: 'GetVideo', 207 | sets: ['videos'], 208 | params: [{ name: 'Id', type: TYPES.Int, value: id }] 209 | }, function (err, result) { 210 | if (err) return logError(err, cb); 211 | 212 | if (result.videos.length) { 213 | return cb(null, normalizeVideoRow(result.videos[0])); 214 | } 215 | 216 | return cb(null, {}); 217 | }); 218 | } 219 | 220 | function getUserById(id, cb) { 221 | return getDataSets({ 222 | sproc: 'GetUserById', 223 | sets: ['users'], 224 | params: [{ name: 'Id', type: TYPES.Int, value: id}] 225 | }, function (err, result) { 226 | if (err) return logError(err, cb); 227 | 228 | if (result.users.length) { 229 | return cb(null, result.users[0]); 230 | } 231 | 232 | return cb(null, {}); 233 | }); 234 | } 235 | 236 | function getUserByEmail(email, cb) { 237 | return getDataSets({ 238 | sproc: 'GetUserByEmail', 239 | sets: ['users'], 240 | params: [{ name: 'Email', type: TYPES.VarChar, value: email }] 241 | }, function (err, result) { 242 | if (err) return logError(err, cb); 243 | 244 | if (result.users && result.users.length) { 245 | return cb(null, result.users[0]); 246 | } 247 | 248 | return cb(); 249 | }); 250 | } 251 | 252 | function createOrModifyVideo(req, cb) { 253 | connect(function (err, connection) { 254 | if (err) return cb(err); 255 | 256 | try { 257 | var resultVideoId; 258 | 259 | var request = new tedious.Request('UpsertVideo', function (err) { 260 | if (err) return logError(err, cb); 261 | return cb(null, { videoId: resultVideoId }); 262 | }); 263 | 264 | if (req.id) 265 | request.addParameter('Id', TYPES.Int, req.id); 266 | 267 | request.addParameter('Name', TYPES.NVarChar, req.name); 268 | request.addParameter('Width', TYPES.Int, req.width); 269 | request.addParameter('Height', TYPES.Int, req.height); 270 | request.addParameter('DurationSeconds', TYPES.Real, req.durationSeconds); 271 | request.addParameter('FramesPerSecond', TYPES.Real, req.framesPerSecond); 272 | 273 | var table = { 274 | columns: [ { name: '[Name]', type: TYPES.VarChar } ], 275 | rows: [] 276 | }; 277 | 278 | for (var i=0; i < req.labels.length; i++) { 279 | table.rows.push([req.labels[i]]); 280 | } 281 | request.addParameter('udtLabels', TYPES.TVP, table); 282 | 283 | if (req.videoJson) 284 | request.addParameter('VideoJson', TYPES.NVarChar, JSON.stringify(req.videoJson)); 285 | 286 | request.addOutputParameter('VideoId', TYPES.Int); 287 | 288 | request.on('returnValue', function (parameterName, value, metadata) { 289 | if (parameterName == 'VideoId') { 290 | resultVideoId = value; 291 | } 292 | }); 293 | 294 | connection.callProcedure(request); 295 | } 296 | catch (err) { 297 | return logError(err, cb); 298 | } 299 | 300 | }); 301 | } 302 | 303 | function createOrModifyJob(req, cb) { 304 | connect(function(err, connection){ 305 | if (err) return logError(err, cb); 306 | 307 | try 308 | { 309 | var resultJobId; 310 | 311 | var request = new tedious.Request('UpsertJob', function(err) { 312 | if (err && err.number == DBErrors.duplicate) return logError(new Error('video already assigned to user'), cb); 313 | if (err) return logError(err, cb); 314 | 315 | return cb(null, {jobId: resultJobId}); 316 | }); 317 | 318 | if(req.id) 319 | request.addParameter('Id', TYPES.Int, req.id); 320 | 321 | request.addParameter('VideoId', TYPES.Int, req.videoId); 322 | request.addParameter('UserId', TYPES.Int, req.userId); 323 | request.addParameter('StatusId', TYPES.TinyInt, req.statusId); 324 | request.addParameter('Description', TYPES.VarChar, req.description); 325 | request.addParameter('CreatedById', TYPES.Int, req.createdById); 326 | 327 | if(req.configJson) 328 | request.addParameter('ConfigJson', TYPES.NVarChar, JSON.stringify(req.configJson)); 329 | 330 | request.addOutputParameter('JobId', TYPES.Int); 331 | 332 | request.on('returnValue', function(parameterName, value, metadata) { 333 | if (parameterName == 'JobId') { 334 | resultJobId = value; 335 | } 336 | }); 337 | 338 | connection.callProcedure(request); 339 | } 340 | catch(err) { 341 | return logError(err, cb); 342 | } 343 | 344 | }); 345 | } 346 | 347 | function getDataSets(opts, cb) { 348 | connect(function(err, connection){ 349 | if (err) return logError(err, cb); 350 | 351 | var sproc = opts.sproc, 352 | sets = opts.sets, 353 | params = opts.params, 354 | currSetIndex = -1; 355 | 356 | var result = {}; 357 | 358 | var request = new tedious.Request(sproc, function(err, rowCount, rows) { 359 | if (err) return logError(err, cb); 360 | }); 361 | 362 | for (var i=0; i createdb.log'; 34 | child_process.exec(createdb_command, null, function (error, stdout, stderr) { 35 | 36 | if (error) return console.error('Error creating new DB schema', error); 37 | 38 | 39 | //insert admin user 40 | var insertAdmin = 'sqlcmd -U %DB_USER% -S %DB_SERVER% -P %DB_PASSWORD% -d %DB_NAME% -Q ' + '"INSERT INTO [dbo].[Users] ([Name] ,[Email] ,[RoleId]) VALUES (\'%DB_USER%\' ,\'%DB_EMAIL%\' ,2)"'; 41 | child_process.exec(insertAdmin, null, function (error, stdout, stderr) { 42 | 43 | if (error) return console.error('Error inserting admin', error); 44 | return console.info('DB schema deployed successfully'); 45 | }); 46 | 47 | }); 48 | }); 49 | } -------------------------------------------------------------------------------- /storage/sql/schema.sql: -------------------------------------------------------------------------------- 1 | /****** Object: UserDefinedTableType [dbo].[UDT_LabelsList] Script Date: 1/12/2016 1:42:58 AM ******/ 2 | CREATE TYPE [dbo].[UDT_LabelsList] AS TABLE( 3 | [Name] [varchar](50) NULL 4 | ) 5 | GO 6 | /****** Object: UserDefinedTableType [dbo].[UDT_VideoLabelsList] Script Date: 1/12/2016 1:42:58 AM ******/ 7 | CREATE TYPE [dbo].[UDT_VideoLabelsList] AS TABLE( 8 | [LabelId] [int] NULL 9 | ) 10 | GO 11 | /****** Object: Table [dbo].[Frames] Script Date: 1/12/2016 1:42:58 AM ******/ 12 | SET ANSI_NULLS ON 13 | GO 14 | SET QUOTED_IDENTIFIER ON 15 | GO 16 | CREATE TABLE [dbo].[Frames]( 17 | [JobId] [int] NOT NULL, 18 | [FrameIndex] [bigint] NOT NULL, 19 | [TagsJson] [ntext] NULL, 20 | CONSTRAINT [PK_Frames] PRIMARY KEY CLUSTERED 21 | ( 22 | [JobId] ASC, 23 | [FrameIndex] ASC 24 | ) 25 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 26 | 27 | GO 28 | /****** Object: Table [dbo].[Jobs] Script Date: 1/12/2016 1:42:58 AM ******/ 29 | SET ANSI_NULLS ON 30 | GO 31 | SET QUOTED_IDENTIFIER ON 32 | GO 33 | CREATE TABLE [dbo].[Jobs]( 34 | [Id] [int] IDENTITY(1,1) NOT NULL, 35 | [VideoId] [int] NOT NULL, 36 | [UserId] [int] NOT NULL, 37 | [Description] [nvarchar](1024) NULL, 38 | [CreatedById] [int] NULL, 39 | [ReviewedById] [int] NULL, 40 | [ConfigJson] [ntext] NULL, 41 | [CreateDate] [datetime] NOT NULL, 42 | [StatusId] [tinyint] NOT NULL, 43 | CONSTRAINT [PK_Jobs] PRIMARY KEY CLUSTERED 44 | ( 45 | [Id] ASC 46 | ) 47 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 48 | 49 | GO 50 | /****** Object: Table [dbo].[JobStatus] Script Date: 1/12/2016 1:42:58 AM ******/ 51 | SET ANSI_NULLS ON 52 | GO 53 | SET QUOTED_IDENTIFIER ON 54 | GO 55 | CREATE TABLE [dbo].[JobStatus]( 56 | [Id] [tinyint] NOT NULL, 57 | [Name] [nvarchar](50) NOT NULL, 58 | [Description] [nvarchar](100) NULL, 59 | CONSTRAINT [PK_JobStatus] PRIMARY KEY CLUSTERED 60 | ( 61 | [Id] ASC 62 | ) 63 | ) ON [PRIMARY] 64 | 65 | GO 66 | /****** Object: Table [dbo].[Labels] Script Date: 1/12/2016 1:42:58 AM ******/ 67 | SET ANSI_NULLS ON 68 | GO 69 | SET QUOTED_IDENTIFIER ON 70 | GO 71 | CREATE TABLE [dbo].[Labels]( 72 | [Id] [int] IDENTITY(1,1) NOT NULL, 73 | [Name] [nvarchar](50) NOT NULL, 74 | CONSTRAINT [PK_Labels] PRIMARY KEY CLUSTERED 75 | ( 76 | [Id] ASC 77 | ), 78 | CONSTRAINT [IX_Labels_Name] UNIQUE NONCLUSTERED 79 | ( 80 | [Name] ASC 81 | ) 82 | ) ON [PRIMARY] 83 | 84 | GO 85 | /****** Object: Table [dbo].[Roles] Script Date: 1/12/2016 1:42:58 AM ******/ 86 | SET ANSI_NULLS ON 87 | GO 88 | SET QUOTED_IDENTIFIER ON 89 | GO 90 | SET ANSI_PADDING ON 91 | GO 92 | CREATE TABLE [dbo].[Roles]( 93 | [Id] [tinyint] NOT NULL, 94 | [Name] [varchar](50) NOT NULL, 95 | [Description] [nchar](1024) NULL, 96 | CONSTRAINT [PK_Roles] PRIMARY KEY CLUSTERED 97 | ( 98 | [Id] ASC 99 | ) 100 | ) ON [PRIMARY] 101 | 102 | GO 103 | SET ANSI_PADDING OFF 104 | GO 105 | /****** Object: Table [dbo].[Users] Script Date: 1/12/2016 1:42:58 AM ******/ 106 | SET ANSI_NULLS ON 107 | GO 108 | SET QUOTED_IDENTIFIER ON 109 | GO 110 | SET ANSI_PADDING ON 111 | GO 112 | CREATE TABLE [dbo].[Users]( 113 | [Id] [int] IDENTITY(1,1) NOT NULL, 114 | [Name] [nvarchar](50) NOT NULL, 115 | [Email] [varchar](100) NOT NULL, 116 | [RoleId] [tinyint] NOT NULL, 117 | CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED 118 | ( 119 | [Id] ASC 120 | ) 121 | ) ON [PRIMARY] 122 | 123 | GO 124 | SET ANSI_PADDING OFF 125 | GO 126 | /****** Object: Table [dbo].[VideoLabels] Script Date: 1/12/2016 1:42:58 AM ******/ 127 | SET ANSI_NULLS ON 128 | GO 129 | SET QUOTED_IDENTIFIER ON 130 | GO 131 | CREATE TABLE [dbo].[VideoLabels]( 132 | [VideoId] [int] NOT NULL, 133 | [LabelId] [int] NOT NULL, 134 | CONSTRAINT [PK_VideoLabels] PRIMARY KEY CLUSTERED 135 | ( 136 | [VideoId] ASC, 137 | [LabelId] ASC 138 | ) 139 | ) ON [PRIMARY] 140 | 141 | GO 142 | /****** Object: Table [dbo].[Videos] Script Date: 1/12/2016 1:42:58 AM ******/ 143 | SET ANSI_NULLS ON 144 | GO 145 | SET QUOTED_IDENTIFIER ON 146 | GO 147 | CREATE TABLE [dbo].[Videos]( 148 | [Id] [int] IDENTITY(1,1) NOT NULL, 149 | [Name] [nvarchar](100) NOT NULL, 150 | [Width] [int] NOT NULL, 151 | [Height] [int] NOT NULL, 152 | [DurationSeconds] [real] NOT NULL, 153 | [FramesPerSecond] [real] NOT NULL, 154 | [VideoJson] [ntext] NULL, 155 | [VideoUploaded] [bit] NULL, 156 | CONSTRAINT [PK_Videos] PRIMARY KEY CLUSTERED 157 | ( 158 | [Id] ASC 159 | ) 160 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 161 | 162 | GO 163 | /****** Object: Index [IX_Jobs] Script Date: 1/12/2016 1:42:58 AM ******/ 164 | CREATE UNIQUE NONCLUSTERED INDEX [IX_Jobs] ON [dbo].[Jobs] 165 | ( 166 | [UserId] ASC, 167 | [VideoId] ASC 168 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 169 | GO 170 | SET ANSI_PADDING ON 171 | 172 | GO 173 | /****** Object: Index [IX_Users_Email] Script Date: 1/12/2016 1:42:58 AM ******/ 174 | CREATE UNIQUE NONCLUSTERED INDEX [IX_Users_Email] ON [dbo].[Users] 175 | ( 176 | [Email] ASC 177 | )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 178 | GO 179 | ALTER TABLE [dbo].[Frames] WITH CHECK ADD CONSTRAINT [FK_Frames_Jobs] FOREIGN KEY([JobId]) 180 | REFERENCES [dbo].[Jobs] ([Id]) 181 | GO 182 | ALTER TABLE [dbo].[Frames] CHECK CONSTRAINT [FK_Frames_Jobs] 183 | GO 184 | ALTER TABLE [dbo].[Jobs] WITH CHECK ADD CONSTRAINT [FK_Jobs_JobStatus] FOREIGN KEY([StatusId]) 185 | REFERENCES [dbo].[JobStatus] ([Id]) 186 | GO 187 | ALTER TABLE [dbo].[Jobs] CHECK CONSTRAINT [FK_Jobs_JobStatus] 188 | GO 189 | ALTER TABLE [dbo].[Jobs] WITH CHECK ADD CONSTRAINT [FK_Jobs_Users] FOREIGN KEY([UserId]) 190 | REFERENCES [dbo].[Users] ([Id]) 191 | GO 192 | ALTER TABLE [dbo].[Jobs] CHECK CONSTRAINT [FK_Jobs_Users] 193 | GO 194 | ALTER TABLE [dbo].[Jobs] WITH CHECK ADD CONSTRAINT [FK_Jobs_Users_CreatedById] FOREIGN KEY([CreatedById]) 195 | REFERENCES [dbo].[Users] ([Id]) 196 | GO 197 | ALTER TABLE [dbo].[Jobs] CHECK CONSTRAINT [FK_Jobs_Users_CreatedById] 198 | GO 199 | ALTER TABLE [dbo].[Jobs] WITH CHECK ADD CONSTRAINT [FK_Jobs_Users_ReviewedByAdmin] FOREIGN KEY([ReviewedById]) 200 | REFERENCES [dbo].[Users] ([Id]) 201 | GO 202 | ALTER TABLE [dbo].[Jobs] CHECK CONSTRAINT [FK_Jobs_Users_ReviewedByAdmin] 203 | GO 204 | ALTER TABLE [dbo].[Jobs] WITH CHECK ADD CONSTRAINT [FK_Jobs_Videos] FOREIGN KEY([VideoId]) 205 | REFERENCES [dbo].[Videos] ([Id]) 206 | GO 207 | ALTER TABLE [dbo].[Jobs] CHECK CONSTRAINT [FK_Jobs_Videos] 208 | GO 209 | ALTER TABLE [dbo].[Users] WITH CHECK ADD CONSTRAINT [FK_Users_Roles] FOREIGN KEY([RoleId]) 210 | REFERENCES [dbo].[Roles] ([Id]) 211 | GO 212 | ALTER TABLE [dbo].[Users] CHECK CONSTRAINT [FK_Users_Roles] 213 | GO 214 | ALTER TABLE [dbo].[VideoLabels] WITH CHECK ADD CONSTRAINT [FK_VideoLabels_Labels] FOREIGN KEY([LabelId]) 215 | REFERENCES [dbo].[Labels] ([Id]) 216 | GO 217 | ALTER TABLE [dbo].[VideoLabels] CHECK CONSTRAINT [FK_VideoLabels_Labels] 218 | GO 219 | ALTER TABLE [dbo].[VideoLabels] WITH CHECK ADD CONSTRAINT [FK_VideoLabels_Videos] FOREIGN KEY([VideoId]) 220 | REFERENCES [dbo].[Videos] ([Id]) 221 | GO 222 | ALTER TABLE [dbo].[VideoLabels] CHECK CONSTRAINT [FK_VideoLabels_Videos] 223 | GO 224 | /****** Object: StoredProcedure [dbo].[GetAllJobs] Script Date: 1/12/2016 1:42:58 AM ******/ 225 | SET ANSI_NULLS ON 226 | GO 227 | SET QUOTED_IDENTIFIER ON 228 | GO 229 | 230 | CREATE PROCEDURE [dbo].[GetAllJobs] 231 | AS 232 | BEGIN 233 | SELECT j.Id as JobId, j.Description, j.CreatedById, u1.Name as CreatedByName, j.UserId, u3.Name as UserName, j.ReviewedById, u2.Name as ReviewedByName, j.CreateDate, 'TODO' as Progress, 234 | j.ConfigJson, j.StatusId, js.Name as StatusName, v.Id as VideoId, v.Name as VideoName, v.Width, v.Height, v.DurationSeconds, v.FramesPerSecond, v.VideoJson 235 | FROM Jobs j 236 | JOIN Videos v 237 | ON j.VideoId = v.Id 238 | LEFT JOIN USERS u1 ON j.CreatedById = u1.Id 239 | LEFT JOIN USERS u2 ON j.ReviewedById = u2.Id 240 | LEFT JOIN USERS u3 ON j.UserId = u3.Id 241 | JOIN JobStatus js ON js.Id = j.StatusId 242 | END 243 | 244 | 245 | 246 | 247 | 248 | GO 249 | /****** Object: StoredProcedure [dbo].[GetJob] Script Date: 1/12/2016 1:42:58 AM ******/ 250 | SET ANSI_NULLS ON 251 | GO 252 | SET QUOTED_IDENTIFIER ON 253 | GO 254 | CREATE PROCEDURE [dbo].[GetJob] 255 | @Id int 256 | AS 257 | BEGIN 258 | 259 | DECLARE @UserId int = -1 260 | DECLARE @VideoId int = 0 261 | 262 | SELECT @UserId = j.UserId, @VideoId = j.VideoId 263 | FROM Jobs j 264 | WHERE j.Id = @Id 265 | 266 | SELECT * FROM Jobs WHERE Id = @Id 267 | 268 | SELECT * FROM Videos WHERE Id = @VideoId 269 | 270 | SELECT * FROM Users WHERE Id = @UserId 271 | 272 | SELECT * FROM Frames WHERE JobId = @Id 273 | 274 | RETURN 1 275 | 276 | END 277 | 278 | 279 | 280 | 281 | 282 | GO 283 | /****** Object: StoredProcedure [dbo].[GetJobStatuses] Script Date: 1/12/2016 1:42:58 AM ******/ 284 | SET ANSI_NULLS ON 285 | GO 286 | SET QUOTED_IDENTIFIER ON 287 | GO 288 | CREATE PROCEDURE [dbo].[GetJobStatuses] 289 | AS 290 | BEGIN 291 | 292 | SELECT * 293 | FROM JobStatus 294 | 295 | END 296 | 297 | 298 | 299 | GO 300 | /****** Object: StoredProcedure [dbo].[GetLabels] Script Date: 1/12/2016 1:42:58 AM ******/ 301 | SET ANSI_NULLS ON 302 | GO 303 | SET QUOTED_IDENTIFIER ON 304 | GO 305 | CREATE PROCEDURE [dbo].[GetLabels] 306 | AS 307 | BEGIN 308 | SELECT * FROM [dbo].[Labels] ORDER BY [Labels].[Name] ASC 309 | END 310 | 311 | GO 312 | /****** Object: StoredProcedure [dbo].[GetRoles] Script Date: 1/12/2016 1:42:58 AM ******/ 313 | SET ANSI_NULLS ON 314 | GO 315 | SET QUOTED_IDENTIFIER ON 316 | GO 317 | CREATE PROCEDURE [dbo].[GetRoles] 318 | AS 319 | BEGIN 320 | 321 | SELECT * 322 | FROM Roles 323 | 324 | END 325 | 326 | 327 | 328 | GO 329 | /****** Object: StoredProcedure [dbo].[GetUserByEmail] Script Date: 1/12/2016 1:42:58 AM ******/ 330 | SET ANSI_NULLS ON 331 | GO 332 | SET QUOTED_IDENTIFIER ON 333 | GO 334 | CREATE PROCEDURE [dbo].[GetUserByEmail] 335 | @Email varchar(100) 336 | AS 337 | BEGIN 338 | 339 | SELECT u.*, r.Name as RoleName 340 | FROM Users u 341 | JOIN Roles r 342 | ON u.RoleId = r.Id 343 | WHERE u.Email = @Email 344 | 345 | RETURN 1 346 | 347 | END 348 | 349 | 350 | 351 | 352 | 353 | GO 354 | /****** Object: StoredProcedure [dbo].[GetUserById] Script Date: 1/12/2016 1:42:58 AM ******/ 355 | SET ANSI_NULLS ON 356 | GO 357 | SET QUOTED_IDENTIFIER ON 358 | GO 359 | CREATE PROCEDURE [dbo].[GetUserById] 360 | @Id int 361 | AS 362 | BEGIN 363 | 364 | SELECT u.*, r.Name as RoleName 365 | FROM Users u 366 | JOIN Roles r 367 | ON u.RoleId = r.Id 368 | WHERE u.Id = @Id 369 | 370 | RETURN 1 371 | 372 | END 373 | 374 | 375 | 376 | 377 | 378 | GO 379 | /****** Object: StoredProcedure [dbo].[GetUserJobs] Script Date: 1/12/2016 1:42:58 AM ******/ 380 | SET ANSI_NULLS ON 381 | GO 382 | SET QUOTED_IDENTIFIER ON 383 | GO 384 | CREATE PROCEDURE [dbo].[GetUserJobs] 385 | @UserId int 386 | AS 387 | BEGIN 388 | SELECT j.Id as JobId, j.Description, j.CreatedById, u1.Name as CreatedByName, j.UserId, u3.Name as UserName, j.ReviewedById, u2.Name as ReviewedByName, j.CreateDate, 40 as Progress, 389 | j.ConfigJson, j.StatusId, js.Name as StatusName, v.Id as VideoId, v.Name as VideoName, v.Width, v.Height, v.DurationSeconds, v.FramesPerSecond, v.VideoJson 390 | FROM Jobs j 391 | JOIN Videos v 392 | ON j.UserId = @UserId AND j.VideoId = v.Id 393 | LEFT JOIN USERS u1 ON j.CreatedById = u1.Id 394 | LEFT JOIN USERS u2 ON j.ReviewedById = u2.Id 395 | LEFT JOIN USERS u3 ON j.UserId = u3.Id 396 | JOIN JobStatus js ON js.Id = j.StatusId 397 | END 398 | 399 | 400 | 401 | 402 | GO 403 | /****** Object: StoredProcedure [dbo].[GetUsers] Script Date: 1/12/2016 1:42:58 AM ******/ 404 | SET ANSI_NULLS ON 405 | GO 406 | SET QUOTED_IDENTIFIER ON 407 | GO 408 | CREATE PROCEDURE [dbo].[GetUsers] 409 | AS 410 | 411 | BEGIN 412 | 413 | SELECT u.*, r.Name as RoleName 414 | FROM Users u 415 | JOIN Roles r 416 | ON u.RoleId = r.Id 417 | 418 | END 419 | 420 | 421 | 422 | 423 | GO 424 | /****** Object: StoredProcedure [dbo].[GetVideo] Script Date: 1/12/2016 1:42:58 AM ******/ 425 | SET ANSI_NULLS ON 426 | GO 427 | SET QUOTED_IDENTIFIER ON 428 | GO 429 | 430 | 431 | CREATE PROCEDURE [dbo].[GetVideo] 432 | @Id int 433 | AS 434 | BEGIN 435 | SELECT v.*, 436 | stuff( 437 | ( SELECT ','+ l.Name 438 | FROM VideoLabels vl 439 | JOIN Labels l 440 | ON vl.LabelId = l.Id 441 | AND vl.VideoId = v.Id 442 | FOR XML PATH('') 443 | ),1,1,'') AS Labels 444 | FROM (SELECT * FROM Videos) v 445 | WHERE Id = @Id 446 | END 447 | 448 | 449 | 450 | 451 | 452 | 453 | GO 454 | /****** Object: StoredProcedure [dbo].[GetVideoFrames] Script Date: 1/12/2016 1:42:58 AM ******/ 455 | SET ANSI_NULLS ON 456 | GO 457 | SET QUOTED_IDENTIFIER ON 458 | GO 459 | 460 | CREATE PROCEDURE [dbo].[GetVideoFrames] 461 | @VideoId int 462 | AS 463 | 464 | BEGIN 465 | 466 | SELECT f.* 467 | FROM Jobs j 468 | JOIN Frames f 469 | ON j.Id = f.JobId 470 | WHERE j.VideoId = @VideoId 471 | ORDER BY f.FrameIndex ASC 472 | 473 | END 474 | 475 | 476 | 477 | 478 | 479 | GO 480 | /****** Object: StoredProcedure [dbo].[GetVideoFramesByJob] Script Date: 1/12/2016 1:42:58 AM ******/ 481 | SET ANSI_NULLS ON 482 | GO 483 | SET QUOTED_IDENTIFIER ON 484 | GO 485 | 486 | CREATE PROCEDURE [dbo].[GetVideoFramesByJob] 487 | @JobId int 488 | AS 489 | 490 | BEGIN 491 | 492 | SELECT f.* 493 | FROM Jobs j 494 | JOIN Frames f 495 | ON j.Id = f.JobId 496 | WHERE j.Id = @JobId 497 | ORDER BY f.FrameIndex ASC 498 | 499 | END 500 | 501 | 502 | 503 | 504 | 505 | GO 506 | /****** Object: StoredProcedure [dbo].[GetVideos] Script Date: 1/12/2016 1:42:58 AM ******/ 507 | SET ANSI_NULLS ON 508 | GO 509 | SET QUOTED_IDENTIFIER ON 510 | GO 511 | CREATE PROCEDURE [dbo].[GetVideos] 512 | @udtVideoLabels UDT_VideoLabelsList READONLY, 513 | @Unassigned bit = 0 514 | AS 515 | 516 | BEGIN 517 | 518 | 519 | DECLARE @QRY VARCHAR(4000) 520 | SET @QRY = 'SELECT v.*, 521 | stuff( 522 | ( SELECT '',''+ l.Name 523 | FROM VideoLabels vl 524 | JOIN Labels l 525 | ON vl.LabelId = l.Id 526 | AND vl.VideoId = v.Id 527 | FOR XML PATH('''') 528 | ),1,1,'''') AS Labels 529 | FROM (SELECT * FROM Videos) v ' 530 | 531 | IF @Unassigned = 1 532 | BEGIN 533 | SET @QRY = @QRY + 'LEFT JOIN Jobs j ON j.VideoId = v.Id ' 534 | END 535 | 536 | 537 | DECLARE @Cursor CURSOR; 538 | DECLARE @LabelId int 539 | 540 | SET @Cursor = CURSOR FOR 541 | select LabelId from @udtVideoLabels 542 | 543 | OPEN @Cursor 544 | FETCH NEXT FROM @Cursor 545 | INTO @LabelId 546 | 547 | DECLARE @I INT = 1 548 | WHILE @@FETCH_STATUS = 0 549 | BEGIN 550 | 551 | SET @QRY = @QRY + 'JOIN VideoLabels vl' + CAST(@I AS VARCHAR(10)) 552 | + ' ON v.Id = vl' + CAST(@I AS VARCHAR(10)) + '.VideoId AND vl' 553 | + CAST(@I AS VARCHAR(10)) + '.LabelId =' + CAST(@LabelId AS VARCHAR(10)) + ' ' 554 | FETCH NEXT FROM @Cursor 555 | INTO @LabelId 556 | 557 | SET @I = @I + 1 558 | END; 559 | 560 | CLOSE @Cursor ; 561 | DEALLOCATE @Cursor; 562 | 563 | IF @Unassigned = 1 564 | BEGIN 565 | SET @QRY = @QRY + ' WHERE j.VideoId IS NULL' 566 | END 567 | 568 | --SELECT @QRY 569 | EXEC (@QRY) 570 | 571 | 572 | END 573 | 574 | 575 | 576 | 577 | GO 578 | /****** Object: StoredProcedure [dbo].[GetVideosByLabels] Script Date: 1/12/2016 1:42:58 AM ******/ 579 | SET ANSI_NULLS ON 580 | GO 581 | SET QUOTED_IDENTIFIER ON 582 | GO 583 | CREATE PROCEDURE [dbo].[GetVideosByLabels] 584 | @udtVideoLabels UDT_VideoLabelsList READONLY 585 | AS 586 | 587 | BEGIN 588 | 589 | 590 | DECLARE @QRY VARCHAR(4000) 591 | SET @QRY = 'SELECT v.*, 592 | stuff( 593 | ( SELECT '',''+ l.Name 594 | FROM VideoLabels vl 595 | JOIN Labels l 596 | ON vl.LabelId = l.Id 597 | AND vl.VideoId = v.Id 598 | FOR XML PATH('''') 599 | ),1,1,'''') AS Labels 600 | FROM (SELECT * FROM Videos) v ' 601 | 602 | 603 | DECLARE @Cursor CURSOR; 604 | DECLARE @LabelId int 605 | 606 | SET @Cursor = CURSOR FOR 607 | select LabelId from @udtVideoLabels 608 | 609 | OPEN @Cursor 610 | FETCH NEXT FROM @Cursor 611 | INTO @LabelId 612 | 613 | DECLARE @I INT = 1 614 | WHILE @@FETCH_STATUS = 0 615 | BEGIN 616 | 617 | SET @QRY = @QRY + 'JOIN VideoLabels vl' + CAST(@I AS VARCHAR(10)) 618 | + ' ON v.Id = vl' + CAST(@I AS VARCHAR(10)) + '.VideoId AND vl' 619 | + CAST(@I AS VARCHAR(10)) + '.LabelId =' + CAST(@LabelId AS VARCHAR(10)) + ' ' 620 | FETCH NEXT FROM @Cursor 621 | INTO @LabelId 622 | 623 | SET @I = @I + 1 624 | END; 625 | 626 | CLOSE @Cursor ; 627 | DEALLOCATE @Cursor; 628 | 629 | --SELECT @QRY 630 | EXEC (@QRY) 631 | 632 | 633 | END 634 | 635 | 636 | 637 | 638 | GO 639 | /****** Object: StoredProcedure [dbo].[UpdateJobStatus] Script Date: 1/12/2016 1:42:58 AM ******/ 640 | SET ANSI_NULLS ON 641 | GO 642 | SET QUOTED_IDENTIFIER ON 643 | GO 644 | 645 | CREATE PROCEDURE [dbo].[UpdateJobStatus] 646 | @Id int = -1, 647 | @UserId int = -1, 648 | @StatusId tinyint 649 | AS 650 | BEGIN 651 | 652 | SET NOCOUNT ON; 653 | 654 | 655 | if @UserId != -1 AND @StatusId=3 /* APPROVED */ 656 | BEGIN 657 | UPDATE Jobs 658 | SET StatusId = @StatusId, ReviewedById = @UserId 659 | WHERE Id = @Id 660 | END 661 | ELSE 662 | BEGIN 663 | UPDATE Jobs 664 | SET StatusId = @StatusId, ReviewedById = NULL 665 | WHERE Id = @Id 666 | END 667 | 668 | END 669 | 670 | 671 | GO 672 | /****** Object: StoredProcedure [dbo].[UpdateVideoUploaded] Script Date: 1/12/2016 1:42:58 AM ******/ 673 | SET ANSI_NULLS ON 674 | GO 675 | SET QUOTED_IDENTIFIER ON 676 | GO 677 | 678 | CREATE PROCEDURE [dbo].[UpdateVideoUploaded] 679 | @Id int = -1 680 | AS 681 | BEGIN 682 | 683 | UPDATE [Videos] 684 | SET VideoUploaded = 1 685 | WHERE Id = @Id 686 | END 687 | 688 | 689 | 690 | GO 691 | /****** Object: StoredProcedure [dbo].[UpsertFrame] Script Date: 1/12/2016 1:42:58 AM ******/ 692 | SET ANSI_NULLS ON 693 | GO 694 | SET QUOTED_IDENTIFIER ON 695 | GO 696 | CREATE PROCEDURE [dbo].[UpsertFrame] 697 | @JobId int, 698 | @FrameIndex bigint, 699 | @TagsJson ntext 700 | AS 701 | BEGIN 702 | 703 | SET NOCOUNT ON; 704 | 705 | MERGE 706 | Frames 707 | USING ( 708 | VALUES (@JobId, @FrameIndex, @TagsJson) 709 | ) AS source (JobId, FrameIndex, TagsJson) 710 | ON Frames.JobId = source.JobId 711 | AND Frames.FrameIndex = source.FrameIndex 712 | WHEN MATCHED THEN 713 | UPDATE SET TagsJson = source.TagsJson 714 | WHEN NOT MATCHED THEN 715 | INSERT (JobId, FrameIndex, TagsJson) 716 | VALUES (JobId, FrameIndex, TagsJson) 717 | ; --A MERGE statement must be terminated by a semi-colon (;). 718 | 719 | END 720 | 721 | 722 | 723 | GO 724 | /****** Object: StoredProcedure [dbo].[UpsertJob] Script Date: 1/12/2016 1:42:58 AM ******/ 725 | SET ANSI_NULLS ON 726 | GO 727 | SET QUOTED_IDENTIFIER ON 728 | GO 729 | 730 | CREATE PROCEDURE [dbo].[UpsertJob] 731 | -- Add the parameters for the stored procedure here 732 | @Id int = -1, 733 | @VideoId int, 734 | @UserId int, 735 | @Description nvarchar(1024), 736 | @CreatedById int, 737 | @StatusId tinyint, 738 | @ConfigJson ntext, 739 | @JobId int OUTPUT 740 | AS 741 | BEGIN 742 | -- SET NOCOUNT ON added to prevent extra result sets from 743 | -- interfering with SELECT statements. 744 | SET NOCOUNT ON; 745 | 746 | BEGIN TRANSACTION T1 747 | 748 | 749 | IF @Id IS NOT NULL AND EXISTS ( 750 | SELECT * FROM Jobs 751 | WHERE Id = @Id 752 | ) 753 | BEGIN 754 | UPDATE [Jobs] 755 | SET VideoId = @VideoId 756 | ,UserId = @UserId 757 | ,[Description] = @Description 758 | ,CreatedById = @CreatedById 759 | ,StatusId = @StatusId 760 | ,ConfigJson = @ConfigJson 761 | WHERE Id = @Id 762 | 763 | SET @JobId = @Id 764 | END 765 | ELSE 766 | BEGIN 767 | 768 | INSERT INTO [dbo].[Jobs] 769 | ([VideoId] 770 | ,[UserId] 771 | ,[Description] 772 | ,[CreatedById] 773 | ,[StatusId] 774 | ,[CreateDate] 775 | ,[ConfigJson] 776 | ) 777 | VALUES 778 | (@VideoId 779 | ,@UserId 780 | ,@Description 781 | ,@CreatedById 782 | ,@StatusId 783 | ,GETDATE() 784 | ,@ConfigJson) 785 | END 786 | 787 | SET @JobId = (SELECT Id FROM Jobs WHERE VideoId = @VideoId AND UserId = @UserId) 788 | 789 | COMMIT TRANSACTION T1 790 | 791 | END 792 | 793 | 794 | 795 | 796 | GO 797 | /****** Object: StoredProcedure [dbo].[UpsertUser] Script Date: 1/12/2016 1:42:58 AM ******/ 798 | SET ANSI_NULLS ON 799 | GO 800 | SET QUOTED_IDENTIFIER ON 801 | GO 802 | 803 | CREATE PROCEDURE [dbo].[UpsertUser] 804 | @Id int = -1, 805 | @Name nvarchar(50), 806 | @Email varchar(100), 807 | @RoleId tinyint, 808 | @UserId int OUTPUT 809 | AS 810 | BEGIN 811 | 812 | IF @Id IS NOT NULL AND EXISTS ( 813 | SELECT * FROM Users 814 | WHERE Id = @Id 815 | ) 816 | BEGIN 817 | UPDATE [Users] 818 | SET Name = @Name 819 | ,Email = @Email 820 | ,RoleId = @RoleId 821 | WHERE Id = @Id 822 | 823 | SET @UserId = @Id 824 | END 825 | ELSE 826 | BEGIN 827 | 828 | INSERT INTO [dbo].[Users] 829 | ([Name] 830 | ,[Email] 831 | ,[RoleId]) 832 | VALUES 833 | (@Name 834 | ,@Email 835 | ,@RoleId) 836 | 837 | SET @UserId = (SELECT @@IDENTITY) 838 | END 839 | 840 | END 841 | 842 | 843 | 844 | 845 | GO 846 | /****** Object: StoredProcedure [dbo].[UpsertVideo] Script Date: 1/12/2016 1:42:58 AM ******/ 847 | SET ANSI_NULLS ON 848 | GO 849 | SET QUOTED_IDENTIFIER ON 850 | GO 851 | 852 | 853 | CREATE PROCEDURE [dbo].[UpsertVideo] 854 | -- Add the parameters for the stored procedure here 855 | @Id int = -1, 856 | @Name nvarchar(100), 857 | @Width int, 858 | @Height int, 859 | @DurationSeconds real, 860 | @FramesPerSecond real, 861 | @VideoJson ntext = NULL, 862 | @udtLabels UDT_LabelsList READONLY, 863 | @VideoId int OUTPUT 864 | AS 865 | BEGIN 866 | -- SET NOCOUNT ON added to prevent extra result sets from 867 | -- interfering with SELECT statements. 868 | SET NOCOUNT ON; 869 | 870 | BEGIN TRANSACTION T1 871 | 872 | IF @Id IS NOT NULL AND EXISTS ( 873 | SELECT * FROM Videos 874 | WHERE Id = @Id 875 | ) 876 | BEGIN 877 | UPDATE [Videos] 878 | SET Name = @Name 879 | ,Width = @Width 880 | ,Height = @Height 881 | ,DurationSeconds = @DurationSeconds 882 | ,FramesPerSecond = @FramesPerSecond 883 | ,VideoJson = @VideoJson 884 | WHERE Id = @Id 885 | 886 | SET @VideoId = @Id 887 | END 888 | ELSE 889 | BEGIN 890 | 891 | INSERT INTO [dbo].[Videos] 892 | ([Name] 893 | ,[Width] 894 | ,[Height] 895 | ,[DurationSeconds] 896 | ,[FramesPerSecond] 897 | ,[VideoJson]) 898 | VALUES 899 | (@Name 900 | ,@Width 901 | ,@Height 902 | ,@DurationSeconds 903 | ,@FramesPerSecond 904 | ,@VideoJson) 905 | 906 | 907 | SET @VideoId = (SELECT @@IDENTITY) 908 | END 909 | 910 | DELETE FROM VideoLabels WHERE VideoId = @VideoId 911 | 912 | -- updating labels 913 | DECLARE @Cursor CURSOR; 914 | DECLARE @Label varchar(50) 915 | DECLARE @LabelId int 916 | 917 | SET @Cursor = CURSOR FOR 918 | select Name from @udtLabels 919 | 920 | OPEN @Cursor 921 | FETCH NEXT FROM @Cursor 922 | INTO @Label 923 | 924 | WHILE @@FETCH_STATUS = 0 925 | BEGIN 926 | 927 | select @Label 928 | SET @LabelId = (SELECT Id FROM Labels WHERE Name = @Label) 929 | IF @LabelId IS NULL 930 | BEGIN 931 | INSERT INTO Labels VALUES (@Label) 932 | SET @LabelId = (SELECT @@IDENTITY) 933 | END 934 | 935 | INSERT INTO VideoLabels VALUES (@VideoId, @LabelId) 936 | 937 | FETCH NEXT FROM @Cursor 938 | INTO @Label 939 | 940 | END; 941 | 942 | CLOSE @Cursor ; 943 | DEALLOCATE @Cursor; 944 | 945 | COMMIT TRANSACTION T1 946 | 947 | END 948 | 949 | GO 950 | 951 | INSERT INTO [dbo].[Roles] ([Id], [Name], [Description]) VALUES (1, 'Editor', 'Edit videos') 952 | GO 953 | 954 | INSERT INTO [dbo].[Roles] ([Id] ,[Name] ,[Description]) VALUES (2 ,'Admin' ,'Administrator') 955 | GO 956 | 957 | INSERT INTO [dbo].[JobStatus] ([Id] ,[Name] ,[Description]) VALUES (1 ,'Active' ,'Job is Active') 958 | GO 959 | 960 | INSERT INTO [dbo].[JobStatus] ([Id] ,[Name] ,[Description]) VALUES (2 ,'Pending' ,'Job is Pending') 961 | GO 962 | 963 | INSERT INTO [dbo].[JobStatus] ([Id] ,[Name] ,[Description]) VALUES (3 ,'Approved' ,'Job is Approved') 964 | GO 965 | 966 | 967 | 968 | --------------------------------------------------------------------------------