├── .autotest ├── .document ├── .gitignore ├── LICENSE ├── README.rdoc ├── Rakefile ├── VERSION ├── bin └── enigmamachine ├── enigmamachine.gemspec ├── lib ├── enigmamachine.rb ├── enigmamachine.sqlite3 ├── enigmamachine │ ├── config.ru │ ├── download_queue.rb │ ├── encoding_queue.rb │ ├── models │ │ ├── encoder.rb │ │ ├── encoding_task.rb │ │ └── video.rb │ ├── public │ │ ├── default.css │ │ └── images │ │ │ ├── Enigma-logo.jpg │ │ │ ├── bg01.jpg │ │ │ ├── bg02.jpg │ │ │ ├── bg03.jpg │ │ │ ├── bg04.jpg │ │ │ ├── img02.gif │ │ │ ├── img03.gif │ │ │ ├── img04.gif │ │ │ ├── img05.gif │ │ │ ├── img06.jpg │ │ │ └── spacer.gif │ └── views │ │ ├── encoders │ │ ├── edit.erb │ │ ├── encoder.erb │ │ ├── encoding_task.erb │ │ ├── form.erb │ │ ├── index.erb │ │ ├── new.erb │ │ └── show.erb │ │ ├── encoding_tasks │ │ ├── edit.erb │ │ ├── form.erb │ │ └── new.erb │ │ ├── index.erb │ │ ├── layout.erb │ │ └── videos │ │ ├── form.erb │ │ ├── index.erb │ │ ├── new.erb │ │ └── video.erb ├── ext │ ├── array_ext.rb │ └── partials.rb └── generators │ └── config.yml └── test ├── helper.rb ├── support ├── afile.mpg └── blueprints.rb ├── test_encoder.rb ├── test_encoding_queue.rb ├── test_enigmamachine.rb └── test_video.rb /.autotest: -------------------------------------------------------------------------------- 1 | Autotest.add_hook :initialize do |at| 2 | %w{ .sqlite3 }.each {|exception| at.add_exception(exception)} 3 | end 4 | 5 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## NetBeans 17 | nbproject 18 | 19 | ## PROJECT::GENERAL 20 | coverage 21 | rdoc 22 | pkg 23 | release 24 | 25 | ## PROJECT::SPECIFIC 26 | *.log 27 | *.sqlite3 28 | *.mpg 29 | *.flv 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = enigmamachine 2 | 3 | Enigmamachine is a RESTful web-based video processor which queues and encodes 4 | videos according to target profiles that you define. Videos must be on a 5 | locally mounted filesystem. The processor takes the path to the video, and 6 | executes one or more ffmpeg commands on the video. There is a handy web 7 | interface for defining encoding tasks, and a restful web service which takes 8 | encoding commands. 9 | 10 | Enigmamachine is written using Sinatra, Thin, and Eventmachine. 11 | 12 | == Why would you want one? 13 | 14 | If you're not running a server, you probably don't need enigmamachine, because 15 | there are already lots of great client-side batch processors for all kinds of 16 | different operating systems. However, if you are running a web application, 17 | and you want to process uploaded video, enigmamachine offers you a convenient 18 | fire-and-forget video encoder. 19 | 20 | The main problem with encoding video on a server is that it takes a really, 21 | really long time - you don't want to do it within the scope of a single HTTP 22 | request, because you want the user's browser to return to their control as 23 | soon as the upload is finished. If the video took ten minutes to upload, 24 | you don't want to keep their browser (and your webapp) busy for a 25 | further ten minutes while the encoding happens. 26 | 27 | The right way to deal with the uploaded video is to fire up a new thread which 28 | can take over responsibility for encoding the video, while your web app goes 29 | on its merry way. Unfortunately, this is difficult in some languages (PHP, for 30 | example, doesn't have lightweight threading support), and even in languages 31 | with good threading support it still sort of sucks. With Enigmamachine, all you 32 | need to do to trigger encoding of a video is shoot off an HTTP request, and 33 | everything else is handled for you. 34 | 35 | 36 | == Using it 37 | 38 | Once you've installed the gem (see below), you can do something like: 39 | 40 | mkdir enigma 41 | cd enigma 42 | enigmamachine start # -d to daemonize 43 | 44 | Then check it out in your browser, at http://localhost:2002. This web interface 45 | exists so that you can configure your enigmamachine easily, and check its status. 46 | 47 | Most of the time, though, your enigmamachine will be accessed not through a 48 | browser, but by your program's code. Let's say you want your website to be able 49 | to encode video. You write the code for the video upload, so your web app can 50 | receive the video. When the upload is complete, make an HTTP call something like this in your code: 51 | 52 | POST http://localhost:2002/videos 53 | 54 | with the params: 55 | 56 | "video[file]=/foo/bar/blah.mp4" # the full path to a file on your local filesystem 57 | "video[callback_url]=http://example.org/call/back/id" # an optional callback url 58 | "encoder_id=1" # the id of an encoder defined in your enigmamachine database 59 | 60 | The enigmamachine will run all encoding tasks on the video, while your web 61 | application will be free to continue serving requests happily. If a second video 62 | is uploaded while the first one is still encoding, it will be placed in a queue. 63 | Videos are encoded sequentially as they arrive. 64 | 65 | All requests, whether they come from your browser or from your code, are 66 | protected by HTTP basic auth. By default, the username is 67 | admin and the password is admin. You can change the username and 68 | password in the config.yml file which will be generated on first use. 69 | 70 | When a video has finished encoding, the main web application might want to know. 71 | The optional @video[callback_url]@ parameter tells enigmamachine to execute a GET 72 | request to the callback url when video encoding is complete. 73 | 74 | Programmatic requests in Ruby might look something like this: 75 | 76 | # enigma_client.rb: 77 | 78 | require 'rubygems' 79 | require 'httparty' 80 | 81 | class EnigmaClient 82 | 83 | include HTTParty 84 | 85 | base_uri "localhost:2002" 86 | 87 | def initialize (u, p) 88 | @auth = encode_credentials(u, p) 89 | end 90 | 91 | def post(path_to_video, encoder_id, callback_url) 92 | self.class.post("/videos", { 93 | :body => {:video => { 94 | :file => path_to_video, 95 | :callback_url => callback_url 96 | }, :encoder_id => encoder_id}, 97 | :basic_auth => @auth}) 98 | end 99 | 100 | private 101 | 102 | def encode_credentials(username, password) 103 | {:username => username, :password => password} 104 | end 105 | 106 | end 107 | 108 | 109 | # Let's use it! 110 | # 111 | irb 112 | require 'enigma_client' 113 | EnigmaClient.new("admin", "admin").post("/path/to/your/uploaded/video.mp4", 1, "http://example.org/call/back/id") 114 | 115 | 116 | A simpler, wget-based approach might look like this: 117 | 118 | wget http://admin:admin@localhost:2002/videos --post-data 'video[file]=/path/to/your/video.mp4&video[callback_url]=http://example.org/call/back/id&encoder_id=1' 119 | 120 | Don't use wget like this on a shared host, it's insecure (read the wget 121 | docs for more on this). This wget example is just to show you what's going on 122 | when using a simple tool that most programmers know how to use. 123 | 124 | == Encoders and Encoding Tasks 125 | 126 | When you POST the location of a video to your enigmamachine, you need to tell 127 | your EM what you want to do to the video. For example, you might want to take 128 | an uploaded video and encode it as a 320x240 FLV video, with a 320x240 JPEG 129 | thumbnail, and a 160x120 miniature thumbnail. 130 | 131 | To do this, you'd fire up your enigmamachine by typing 132 | 133 | enigmamachine start 134 | 135 | and go to http://localhost:2002/encoders. Clicking the "new encoder" link will 136 | allow you to define a new encoder. Let's call it "Flash video with a couple 137 | thumbnails", and save it. 138 | 139 | So, now there's an encoder, but it won't do anything. Let's add some tasks, 140 | by clicking on the "new task" link. 141 | 142 | Give the first task a name, like Encode a 320x240 flv at 25 frames per second. 143 | 144 | The output file suffix in this case will be *.flv*. 145 | 146 | The encoding command you'll want to use would be 147 | -ab 128 -ar 22050 -b 500000 -r 25 -s 320x240. This cryptic command string 148 | tells ffmpeg to encode a Flash video at 320x240 pixels, with an audio bitrate 149 | of 128kbps, an audio sample rate of 22.050khz, and a frame rate of 25fps. You 150 | can find out what all the ffmpeg command line switches do by RTFMing at 151 | http://www.ffmpeg.org/ffmpeg-doc.html 152 | 153 | You would go on to define a couple more encoding tasks for your encoder. 154 | Grabbing a screen frame in ffmpeg can be done with the following command-line 155 | switches, which you can put into a task called, let's say, 156 | Grab a 320x240 JPEG thumbnail: 157 | 158 | -ss 00:00:05 -t 00:00:01 -vcodec mjpeg -vframes 1 -an -f rawvideo -s 320x240 159 | 160 | Rinse and repeat for the 160x120 thumbnail. 161 | 162 | == Security considerations 163 | 164 | Enigmamachine is set to bind by default to 127.0.0.1 (your system's loopback) 165 | interface rather than on all network interfaces, so it won't be accessible from 166 | other machines. 167 | 168 | Making an enigmamachine available on an untrusted network (like 169 | the Internet) would be a suicidal move on your part, since the code used to 170 | talk to ffmpeg is a simple backtick exec call and you'll be inviting everyone in 171 | the world to execute commands on your server, with your effective user 172 | permissions. 173 | 174 | When the enigmamachine starts for the first time in a given directory, it will 175 | spit out a config.yml file containing a username and password. All requests 176 | will need to submit this auth information. This should make enigmamachine 177 | reasonably safe to use on shared hosts, just make sure nobody can read the 178 | config file except the user executing the enigmamachine process. 179 | 180 | If you don't know what any of this means, don't run it. I'm not responsible if 181 | your enigmamachine screws up your system, allows people to exploit you, or 182 | eats your mother. 183 | 184 | == Installation 185 | 186 | Enigmamachine is written in Ruby and uses the Sinatra web framework, the 187 | Datamapper ORM library, the Eventmachine event-processing library, and the Thin 188 | webserver. If you're not a Ruby person, you don't need to care about any of this. 189 | Enigmamachine can be triggered by any language that can send a POST request - 190 | so all you PHPistas, python-lovers, droopy-moustachists, or blue-suited 191 | java types can all use it just as easy as us fashionable-haircut-language 192 | people. 193 | 194 | You can install it as a gem by doing: 195 | 196 | gem install enigmamachine 197 | 198 | If this command doesn't make any sense to you, it's because you don't know that 199 | "gems" are ruby code packages, somewhat like apt-get except for ruby code only. 200 | You can install rubygems, necessary sqlite headers, and a C compiler on 201 | righteous operating systems by typing: 202 | 203 | apt-get install rubygems ruby1.8-dev libopenssl-ruby build-essential libsqlite3-dev ffmpeg # as root 204 | 205 | You'll need to add the following line to your ~/.bashrc file, as well: 206 | 207 | export PATH=/var/lib/gems/1.8/bin:$PATH 208 | 209 | Then gem install enigmamachine should theoretically work. You'll also need a copy of 210 | ffmpeg installed and available in your path. On Mac OS X, rubygems should 211 | already be installed, although you'll need to have a C compiler available 212 | (make sure you install the developer tools that came with your operating 213 | system). 214 | 215 | == Status 216 | 217 | Enigmamachine should be considered beta quality code. At the same time, 218 | crashes and bugs have been infrequent for us, and it's a simple system which 219 | is working well. Please let us know of any bugs by filing an issue on the 220 | GitHub tracker. 221 | 222 | 223 | == Note on Patches/Pull Requests 224 | 225 | * Fork the project. 226 | * Make your feature addition or bug fix. 227 | * Add tests for it. This is important so I don't break it in a 228 | future version unintentionally. 229 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 230 | * Send me a pull request. Bonus points for topic branches. 231 | 232 | == Copyright 233 | 234 | Copyright (c) 2010 Dave Hrycyszyn. See LICENSE for details. 235 | 236 | Enigmamachine is a headlabs project. See http://labs.headlondon.com for more. 237 | 238 | == Contributors 239 | 240 | Dave Hrycyszyn, Dmitry Brazhkin 241 | 242 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "enigmamachine" 8 | gem.summary = %Q{A RESTful video encoder.} 9 | gem.description = %Q{A RESTful video encoder which you can use as either a front-end to ffmpeg or headless on a server.} 10 | gem.email = "dave@caprica" 11 | gem.homepage = "http://github.com/futurechimp/enigmamachine" 12 | gem.authors = ["Dave Hrycyszyn", "Dmitry Brazhkin"] 13 | gem.add_development_dependency "shoulda" 14 | gem.add_development_dependency "rack-test" 15 | gem.add_dependency "data_mapper", ">=1.0.2" 16 | gem.add_dependency "dm-sqlite-adapter", ">=1.0.2" 17 | gem.add_dependency "state_machine", ">=0.9.4" 18 | gem.add_dependency "eventmachine", ">=0.12.10" 19 | gem.add_dependency "sinatra-flash" 20 | gem.add_dependency "ruby-debug" 21 | gem.add_dependency "sinatra", ">=1.0.0" 22 | gem.add_dependency "streamio-ffmpeg", ">=0.7.3" 23 | gem.add_dependency "thin" 24 | gem.add_dependency "em-http-request" 25 | gem.add_dependency "activesupport", "=2.3.12" 26 | 27 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 28 | end 29 | Jeweler::GemcutterTasks.new 30 | rescue LoadError 31 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 32 | end 33 | 34 | require 'rake/testtask' 35 | Rake::TestTask.new(:test) do |test| 36 | test.libs << 'lib' << 'test' 37 | test.pattern = 'test/**/test_*.rb' 38 | test.verbose = true 39 | end 40 | 41 | begin 42 | require 'rcov/rcovtask' 43 | Rcov::RcovTask.new do |test| 44 | test.libs << 'test' 45 | test.pattern = 'test/**/test_*.rb' 46 | test.verbose = true 47 | end 48 | rescue LoadError 49 | task :rcov do 50 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 51 | end 52 | end 53 | 54 | task :test => :check_dependencies 55 | 56 | task :default => :test 57 | 58 | require 'rake/rdoctask' 59 | Rake::RDocTask.new do |rdoc| 60 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 61 | 62 | rdoc.rdoc_dir = 'rdoc' 63 | rdoc.title = "enigmamachine #{version}" 64 | rdoc.rdoc_files.include('README*') 65 | rdoc.rdoc_files.include('lib/**/*.rb') 66 | end 67 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.5 -------------------------------------------------------------------------------- /bin/enigmamachine: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Enigmamachine command line interface script. 4 | # Run enigmamachine -h to get more usage. 5 | require 'rubygems' 6 | require 'thin' 7 | 8 | rackup_file = "#{File.dirname(__FILE__)}/../lib/enigmamachine/config.ru" 9 | 10 | argv = ARGV 11 | argv << ["-R", rackup_file] unless ARGV.include?("-R") 12 | argv << ["-p", "2002"] unless ARGV.include?("-p") 13 | argv << ["-e", "production"] unless ARGV.include?("-e") 14 | Thin::Runner.new(argv.flatten).run! 15 | 16 | -------------------------------------------------------------------------------- /enigmamachine.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "enigmamachine" 8 | s.version = "0.6.5" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Dave Hrycyszyn", "Dmitry Brazhkin"] 12 | s.date = "2012-07-21" 13 | s.description = "A RESTful video encoder which you can use as either a front-end to ffmpeg or headless on a server." 14 | s.email = "dave@caprica" 15 | s.executables = ["enigmamachine"] 16 | s.extra_rdoc_files = [ 17 | "LICENSE", 18 | "README.rdoc" 19 | ] 20 | s.files = [ 21 | ".autotest", 22 | ".document", 23 | "LICENSE", 24 | "README.rdoc", 25 | "Rakefile", 26 | "VERSION", 27 | "bin/enigmamachine", 28 | "enigmamachine.gemspec", 29 | "lib/enigmamachine.rb", 30 | "lib/enigmamachine.sqlite3", 31 | "lib/enigmamachine/config.ru", 32 | "lib/enigmamachine/download_queue.rb", 33 | "lib/enigmamachine/encoding_queue.rb", 34 | "lib/enigmamachine/models/encoder.rb", 35 | "lib/enigmamachine/models/encoding_task.rb", 36 | "lib/enigmamachine/models/video.rb", 37 | "lib/enigmamachine/public/default.css", 38 | "lib/enigmamachine/public/images/Enigma-logo.jpg", 39 | "lib/enigmamachine/public/images/bg01.jpg", 40 | "lib/enigmamachine/public/images/bg02.jpg", 41 | "lib/enigmamachine/public/images/bg03.jpg", 42 | "lib/enigmamachine/public/images/bg04.jpg", 43 | "lib/enigmamachine/public/images/img02.gif", 44 | "lib/enigmamachine/public/images/img03.gif", 45 | "lib/enigmamachine/public/images/img04.gif", 46 | "lib/enigmamachine/public/images/img05.gif", 47 | "lib/enigmamachine/public/images/img06.jpg", 48 | "lib/enigmamachine/public/images/spacer.gif", 49 | "lib/enigmamachine/views/encoders/edit.erb", 50 | "lib/enigmamachine/views/encoders/encoder.erb", 51 | "lib/enigmamachine/views/encoders/encoding_task.erb", 52 | "lib/enigmamachine/views/encoders/form.erb", 53 | "lib/enigmamachine/views/encoders/index.erb", 54 | "lib/enigmamachine/views/encoders/new.erb", 55 | "lib/enigmamachine/views/encoders/show.erb", 56 | "lib/enigmamachine/views/encoding_tasks/edit.erb", 57 | "lib/enigmamachine/views/encoding_tasks/form.erb", 58 | "lib/enigmamachine/views/encoding_tasks/new.erb", 59 | "lib/enigmamachine/views/index.erb", 60 | "lib/enigmamachine/views/layout.erb", 61 | "lib/enigmamachine/views/videos/form.erb", 62 | "lib/enigmamachine/views/videos/index.erb", 63 | "lib/enigmamachine/views/videos/new.erb", 64 | "lib/enigmamachine/views/videos/video.erb", 65 | "lib/ext/array_ext.rb", 66 | "lib/ext/partials.rb", 67 | "lib/generators/config.yml", 68 | "test/helper.rb", 69 | "test/support/afile.mpg", 70 | "test/support/blueprints.rb", 71 | "test/test_encoder.rb", 72 | "test/test_encoding_queue.rb", 73 | "test/test_enigmamachine.rb", 74 | "test/test_video.rb" 75 | ] 76 | s.homepage = "http://github.com/futurechimp/enigmamachine" 77 | s.require_paths = ["lib"] 78 | s.rubygems_version = "1.8.24" 79 | s.summary = "A RESTful video encoder." 80 | 81 | if s.respond_to? :specification_version then 82 | s.specification_version = 3 83 | 84 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 85 | s.add_development_dependency(%q, [">= 0"]) 86 | s.add_development_dependency(%q, [">= 0"]) 87 | s.add_runtime_dependency(%q, [">= 1.0.2"]) 88 | s.add_runtime_dependency(%q, [">= 1.0.2"]) 89 | s.add_runtime_dependency(%q, [">= 0.9.4"]) 90 | s.add_runtime_dependency(%q, [">= 0.12.10"]) 91 | s.add_runtime_dependency(%q, [">= 0"]) 92 | s.add_runtime_dependency(%q, [">= 0"]) 93 | s.add_runtime_dependency(%q, [">= 1.0.0"]) 94 | s.add_runtime_dependency(%q, [">= 0.7.3"]) 95 | s.add_runtime_dependency(%q, [">= 0"]) 96 | s.add_runtime_dependency(%q, [">= 0"]) 97 | s.add_runtime_dependency(%q, ["= 2.3.12"]) 98 | else 99 | s.add_dependency(%q, [">= 0"]) 100 | s.add_dependency(%q, [">= 0"]) 101 | s.add_dependency(%q, [">= 1.0.2"]) 102 | s.add_dependency(%q, [">= 1.0.2"]) 103 | s.add_dependency(%q, [">= 0.9.4"]) 104 | s.add_dependency(%q, [">= 0.12.10"]) 105 | s.add_dependency(%q, [">= 0"]) 106 | s.add_dependency(%q, [">= 0"]) 107 | s.add_dependency(%q, [">= 1.0.0"]) 108 | s.add_dependency(%q, [">= 0.7.3"]) 109 | s.add_dependency(%q, [">= 0"]) 110 | s.add_dependency(%q, [">= 0"]) 111 | s.add_dependency(%q, ["= 2.3.12"]) 112 | end 113 | else 114 | s.add_dependency(%q, [">= 0"]) 115 | s.add_dependency(%q, [">= 0"]) 116 | s.add_dependency(%q, [">= 1.0.2"]) 117 | s.add_dependency(%q, [">= 1.0.2"]) 118 | s.add_dependency(%q, [">= 0.9.4"]) 119 | s.add_dependency(%q, [">= 0.12.10"]) 120 | s.add_dependency(%q, [">= 0"]) 121 | s.add_dependency(%q, [">= 0"]) 122 | s.add_dependency(%q, [">= 1.0.0"]) 123 | s.add_dependency(%q, [">= 0.7.3"]) 124 | s.add_dependency(%q, [">= 0"]) 125 | s.add_dependency(%q, [">= 0"]) 126 | s.add_dependency(%q, ["= 2.3.12"]) 127 | end 128 | end 129 | 130 | -------------------------------------------------------------------------------- /lib/enigmamachine.rb: -------------------------------------------------------------------------------- 1 | # Gems 2 | # 3 | require 'rubygems' 4 | require 'sinatra/base' 5 | require 'data_mapper' 6 | require 'ruby-debug' 7 | require 'eventmachine' 8 | require 'sinatra/flash' 9 | require 'dm-validations' 10 | require 'dm-migrations' 11 | require 'state_machine' 12 | require 'logger' 13 | require 'streamio-ffmpeg' 14 | require 'em-http-request' 15 | require 'activesupport' 16 | 17 | # Extensions to Sinatra 18 | # 19 | require File.dirname(__FILE__) + '/ext/partials' 20 | require File.dirname(__FILE__) + '/ext/array_ext' 21 | 22 | # Enigma code 23 | # 24 | require File.dirname(__FILE__) + '/enigmamachine' 25 | require File.dirname(__FILE__) + '/enigmamachine/models/encoder' 26 | require File.dirname(__FILE__) + '/enigmamachine/models/encoding_task' 27 | require File.dirname(__FILE__) + '/enigmamachine/models/video' 28 | require File.dirname(__FILE__) + '/enigmamachine/encoding_queue' 29 | require File.dirname(__FILE__) + '/enigmamachine/download_queue' 30 | 31 | 32 | 33 | class EnigmaMachine < Sinatra::Base 34 | 35 | # Database config 36 | # 37 | configure :production do 38 | db = "sqlite3:///#{Dir.pwd}/enigmamachine.sqlite3" 39 | DataMapper.setup(:default, db) 40 | end 41 | 42 | configure :development do 43 | db = "sqlite3:///#{Dir.pwd}/enigmamachine.sqlite3" 44 | DataMapper.setup(:default, db) 45 | end 46 | 47 | configure :test do 48 | db = "sqlite3::memory:" 49 | DataMapper.setup(:default, db) 50 | @@username = 'admin' 51 | @@password = 'admin' 52 | end 53 | 54 | configure :production, :test, :development do 55 | Video.auto_migrate! unless Video.storage_exists? 56 | Encoder.auto_migrate! unless Encoder.storage_exists? 57 | EncodingTask.auto_migrate! unless EncodingTask.storage_exists? 58 | end 59 | 60 | configure :production, :development do 61 | DataMapper.auto_upgrade! 62 | unless File.exist? File.join(Dir.getwd, 'config.yml') 63 | FileUtils.cp(File.dirname(__FILE__) + '/generators/config.yml', Dir.getwd) 64 | end 65 | config = YAML.load(File.read(Dir.getwd + "/config.yml")) 66 | @@username = config['username'] 67 | @@password = config['password'] 68 | @@threads = config['threads'] 69 | @@enable_http_downloads = config['enable_http_downloads'] 70 | @@download_storage_path = config['download_storage_path'] 71 | end 72 | 73 | # Set the views to the proper path inside the gem 74 | # 75 | set :views, File.dirname(__FILE__) + '/enigmamachine/views' 76 | set :public, File.dirname(__FILE__) + '/enigmamachine/public' 77 | 78 | # Let's bind this thing to localhost only, it'd be suicidal to put it on the 79 | # internet by binding it to all available interfaces. 80 | # 81 | set :bind, 'localhost' 82 | 83 | # Register helpers 84 | # 85 | helpers do 86 | include Sinatra::Partials 87 | alias_method :h, :escape_html 88 | end 89 | 90 | 91 | # Include flash notices 92 | # 93 | use Rack::Session::Cookie 94 | use Rack::MethodOverride 95 | 96 | # Starts the enigma encoding thread. The thread will be reabsorbed into the 97 | # main Sinatra/thin thread once the periodic timer is added. 98 | # 99 | configure do 100 | Thread.new do 101 | until EM.reactor_running? 102 | sleep 1 103 | end 104 | Video.reset_encoding_videos 105 | Video.reset_downloading_videos 106 | encode_queue = EncodingQueue.new 107 | download_queue = DownloadQueue.new 108 | end 109 | end 110 | 111 | # Set up Rack authentication 112 | # 113 | # Provides minimal security for shared hosts. 114 | # 115 | use Rack::Auth::Basic do |username, password| 116 | [username, password] == [@@username, @@password] 117 | end 118 | 119 | # Some accessors for config variables 120 | # 121 | cattr_accessor :download_storage_path, :enable_http_downloads, :threads 122 | 123 | 124 | # Shows the enigma status page. 125 | # 126 | get '/' do 127 | @videos = Video.all(:limit => 50, :order => [:created_at.desc]) 128 | erb :index 129 | end 130 | 131 | # Displays a list of all available encoders. 132 | # 133 | get '/encoders' do 134 | @encoders = Encoder.all 135 | erb :'encoders/index' 136 | end 137 | 138 | 139 | # Displays a form to create a new encoder. 140 | # 141 | get '/encoders/new' do 142 | @encoder = Encoder.new 143 | erb :'encoders/new' 144 | end 145 | 146 | 147 | # Displays an encoder. 148 | # 149 | get '/encoders/:id' do |id| 150 | @encoder = Encoder.get(id) 151 | @encoding_task = EncodingTask.new 152 | erb :'encoders/show' 153 | end 154 | 155 | 156 | # Displays an edit page for an encoder. 157 | # 158 | get '/encoders/:id/edit' do |id| 159 | @encoder = Encoder.get(id) 160 | erb :"encoders/edit" 161 | end 162 | 163 | 164 | # Creates an encoder. 165 | # 166 | post '/encoders' do 167 | @encoder = Encoder.new(params[:encoder]) 168 | if @encoder.save 169 | flash[:notice] = "Encoder created." 170 | redirect "/encoders/#{@encoder.id}" 171 | else 172 | erb :'encoders/new' 173 | end 174 | end 175 | 176 | 177 | # Updates an encoder 178 | # 179 | put '/encoders/:id' do |id| 180 | @encoder = Encoder.get(id) 181 | if @encoder.update(params[:encoder]) 182 | flash[:notice] = "Encoder updated." 183 | redirect '/encoders' 184 | else 185 | erb :"encoders/edit" 186 | end 187 | end 188 | 189 | # Deletes an encoder 190 | # 191 | delete '/encoders/:id' do |id| 192 | @encoder = Encoder.get(id) 193 | @encoder.destroy! 194 | redirect '/encoders' 195 | end 196 | 197 | 198 | # Show a form to make a new encoding task 199 | # 200 | get '/encoding_tasks/new/:encoder_id' do |encoder_id| 201 | @encoding_task = EncodingTask.new 202 | @encoding_task.encoder = Encoder.get(encoder_id) 203 | erb :'encoding_tasks/new' 204 | end 205 | 206 | 207 | # Creates an encoding task. 208 | # 209 | post '/encoding_tasks/:encoder_id' do |encoder_id| 210 | @encoding_task = EncodingTask.new(params[:encoding_task]) 211 | @encoder = Encoder.get(encoder_id) 212 | @encoding_task.encoder = @encoder 213 | if @encoding_task.save 214 | flash[:notice] = "Encoding task created." 215 | redirect "/encoders/#{@encoding_task.encoder.id}" 216 | else 217 | erb :'encoding_tasks/new' 218 | end 219 | end 220 | 221 | 222 | # Gets the edit form for an encoding task 223 | # 224 | get '/encoding_tasks/:id/edit' do |id| 225 | @encoding_task = EncodingTask.get(id) 226 | erb :'encoding_tasks/edit' 227 | end 228 | 229 | 230 | # Updates an encoding task. 231 | # 232 | put '/encoding_tasks/:id' do |id| 233 | @encoding_task = EncodingTask.get(id) 234 | if @encoding_task.update(params[:encoding_task]) 235 | redirect "/encoders/#{@encoding_task.encoder.id}" 236 | else 237 | erb :'encoding_tasks/edit' 238 | end 239 | end 240 | 241 | # Deletes an encoding task. 242 | # 243 | delete '/encoding_tasks/:id' do |id| 244 | @encoding_task = EncodingTask.get(id) 245 | @encoder = @encoding_task.encoder 246 | @encoding_task.destroy! 247 | redirect "/encoders/#{id}" 248 | end 249 | 250 | # Displays a list of available videos. 251 | # 252 | get '/videos' do 253 | @completed_videos = Video.complete 254 | @encoding_videos = Video.encoding 255 | @videos_with_errors = Video.with_encode_errors 256 | @unencoded_videos = Video.unencoded 257 | @downloading_videos = Video.downloading 258 | @waiting_for_download_videos = Video.waiting_for_download 259 | erb :'videos/index' 260 | end 261 | 262 | 263 | # Displays a form for creating a new video 264 | # 265 | get '/videos/new' do 266 | @video = Video.new 267 | @encoders = Encoder.all 268 | erb :'videos/new' 269 | end 270 | 271 | 272 | # Creates a new video 273 | # 274 | post '/videos' do 275 | @video = Video.new(params[:video]) 276 | @encoder = Encoder.get(params[:encoder_id]) 277 | @video.encoder = @encoder 278 | if @video.save 279 | redirect '/videos' 280 | else 281 | @encoders = Encoder.all 282 | erb :'videos/new' 283 | end 284 | end 285 | 286 | # Deletes a video. 287 | # 288 | delete '/videos/:id' do |id| 289 | @video = Video.get(id) 290 | @video.destroy 291 | redirect "/videos" 292 | end 293 | 294 | end 295 | -------------------------------------------------------------------------------- /lib/enigmamachine.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine.sqlite3 -------------------------------------------------------------------------------- /lib/enigmamachine/config.ru: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../enigmamachine' 2 | EnigmaMachine.run! :port => 2002 3 | 4 | -------------------------------------------------------------------------------- /lib/enigmamachine/download_queue.rb: -------------------------------------------------------------------------------- 1 | class DownloadQueue 2 | 3 | # Adds a periodic timer to the Eventmachine reactor loop and immediately 4 | # starts looking for videos to download. 5 | # 6 | def initialize 7 | if EnigmaMachine.enable_http_downloads 8 | EM.add_periodic_timer(5) do 9 | download_next_video 10 | end 11 | end 12 | end 13 | 14 | 15 | # Gets the next waiting_for_download Video from the database and 16 | # starts downloading it. 17 | # 18 | def download_next_video 19 | if Video.waiting_for_download.count > 0 && Video.downloading.count == 0 20 | video = Video.waiting_for_download.first 21 | begin 22 | video.download! 23 | rescue Exception => ex 24 | # don't do anything just yet, until we set up logging properly. 25 | end 26 | end 27 | end 28 | 29 | end 30 | 31 | -------------------------------------------------------------------------------- /lib/enigmamachine/encoding_queue.rb: -------------------------------------------------------------------------------- 1 | class EncodingQueue 2 | 3 | cattr_accessor :currently_encoding 4 | 5 | # Adds a periodic timer to the Eventmachine reactor loop and immediately 6 | # starts looking for unencoded videos. 7 | # 8 | def initialize 9 | EM.add_periodic_timer(5) { 10 | encode_next_video 11 | } 12 | end 13 | 14 | 15 | # Gets the next unencoded Video from the database and starts encoding its file. 16 | # 17 | def encode_next_video 18 | if Video.unencoded.count > 0 && Video.encoding.count < EnigmaMachine.threads 19 | video = Video.unencoded.first 20 | begin 21 | video.encode! 22 | rescue Exception => ex 23 | # don't do anything just yet, until we set up logging properly. 24 | end 25 | end 26 | end 27 | 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/enigmamachine/models/encoder.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/video' 2 | require 'net/http' 3 | require 'uri' 4 | 5 | # An encoding profile which can be applied to a video. It has a name and is 6 | # composed of a bunch of EncodingTasks. 7 | # 8 | # The Encoder class can shell out to FFMPeg and trigger the encoding of a Video. 9 | # 10 | class Encoder 11 | include DataMapper::Resource 12 | 13 | # Properties 14 | # 15 | property :id, Serial 16 | property :name, String, :required => true, :length => (1..254) 17 | 18 | # Associations 19 | # 20 | has n, :encoding_tasks 21 | 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/enigmamachine/models/encoding_task.rb: -------------------------------------------------------------------------------- 1 | # A task which defines how a video will be encoded. 2 | # 3 | class EncodingTask 4 | include DataMapper::Resource 5 | 6 | # Properties 7 | # 8 | property :id, Serial 9 | property :name, String, :required => true, :length => (1..254) 10 | property :output_file_suffix, String, :required => true, :length => (1..254) 11 | property :command, String, :required => true, :length => (1..510) 12 | property :encoder_id, Integer 13 | 14 | # Associations 15 | # 16 | belongs_to :encoder 17 | 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/enigmamachine/models/video.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/encoder' 2 | require 'net/http' 3 | require 'uri' 4 | 5 | # A video which we want to encode. 6 | # 7 | class Video 8 | include DataMapper::Resource 9 | 10 | # Properties 11 | # 12 | property :id, Serial 13 | property :file, String, :required => true, :length => (1..510) 14 | property :created_at, DateTime 15 | property :updated_at, DateTime 16 | property :encoder_id, Integer, :required => true 17 | property :callback_url, String, :length => 255 18 | property :state, String 19 | 20 | # State machine transitions 21 | # 22 | state_machine do 23 | 24 | # State transitions for HTTP-hosted videos 25 | after_transition :on => :download, :do => :do_download 26 | 27 | # States for videos on the local filesystem 28 | after_transition :on => :encode, :do => :do_encode 29 | after_transition :on => :complete, :do => :notify_complete 30 | 31 | event :download do 32 | transition :waiting_for_download => :downloading 33 | end 34 | 35 | event :download_complete do 36 | transition :downloading => :unencoded 37 | end 38 | 39 | event :download_error do 40 | transition :downloading => :download_error 41 | end 42 | 43 | event :reset_download do 44 | transition :downloading => :waiting_for_download 45 | end 46 | 47 | event :encode do 48 | transition :unencoded => :encoding 49 | end 50 | 51 | event :complete do 52 | transition :encoding => :complete 53 | end 54 | 55 | event :reset do 56 | transition :encoding => :unencoded 57 | end 58 | 59 | event :encode_error do 60 | transition :encoding => :encode_error 61 | end 62 | 63 | end 64 | 65 | # Validations 66 | # 67 | validates_uniqueness_of :file, :scope => :encoder_id, 68 | :message => "Same file with same encoder already exists" 69 | validates_with_method :file, :method => :check_file 70 | 71 | # Associations 72 | # 73 | belongs_to :encoder 74 | 75 | # Filters 76 | # 77 | before :destroy, :check_destroy 78 | before :create, :set_initial_state 79 | 80 | default_scope(:default).update(:order => [:created_at.asc]) 81 | 82 | 83 | # Named scope for all videos which are waiting to start encoding. 84 | # 85 | def self.unencoded 86 | all(:state => 'unencoded') 87 | end 88 | 89 | # Named scope for all videos which are currently encoding. Theoretically 90 | # there should only ever be one. 91 | # 92 | def self.encoding 93 | all(:state => 'encoding') 94 | end 95 | 96 | # Named scope giving back all videos with encoding errors. 97 | # 98 | def self.with_encode_errors 99 | all(:state => 'encode_error') 100 | end 101 | 102 | # Named scope giving back all videos which have completed encoding. 103 | # 104 | def self.complete 105 | all(:state => 'complete', :order => [:updated_at.desc]) 106 | end 107 | 108 | # Named scope returning all videos which are not yet downloaded. 109 | # 110 | def self.waiting_for_download 111 | all(:state => 'waiting_for_download') 112 | end 113 | 114 | # Named scope returning all videos which currently downloading. 115 | # 116 | def self.downloading 117 | all(:state => 'downloading') 118 | end 119 | 120 | # Resets all videos currently marked as "encoding" to state "unencoded" 121 | # which is the initial state. 122 | # 123 | # If any videos are marked as "encoding" when the application starts, 124 | # presumably due to an encoding interruption in the last session, they 125 | # should be reset. 126 | # 127 | def self.reset_encoding_videos 128 | Video.encoding.each do |video| 129 | video.reset! 130 | end 131 | end 132 | 133 | def self.reset_downloading_videos 134 | Video.downloading.each do |video| 135 | video.reset_download! 136 | end 137 | end 138 | 139 | private 140 | 141 | # Validation checks for files - we want to ensure that the video file exists, 142 | # and that it can be encoded by ffmpeg. 143 | # 144 | def check_file 145 | if local? 146 | return [false, "Give a file name, not nil"] if self.file.nil? 147 | return [false, "Give a file name, not a blank string"] if self.file.to_s.empty? 148 | return [false, "#{self.file} does not exist"] unless File.exist? self.file 149 | return [false, "#{self.file} is a directory"] if File.directory? self.file 150 | movie = FFMPEG::Movie.new(self.file) 151 | return [false, "#{self.file} is not a media file"] unless movie.valid? 152 | end 153 | return true 154 | end 155 | 156 | # Returns true unless the video's file starts with 'http://' 157 | # 158 | def set_initial_state 159 | if local? 160 | self.state = "unencoded" 161 | else 162 | self.state = "waiting_for_download" 163 | end 164 | end 165 | 166 | # Checks whether a video object can be destroyed - videos cannot be destroyed 167 | # if they are currently encoding. 168 | # 169 | def check_destroy 170 | return true if (self.state != 'encoding') 171 | return true if stop_encode 172 | throw :halt 173 | end 174 | 175 | # Would stop the encoder process if it was implemented. Currently does nothing 176 | # and returns false. 177 | # 178 | def stop_encode 179 | return false 180 | #TODO Kill the encoder process 181 | end 182 | 183 | # Tells this video's encoder to start encoding tasks. 184 | # 185 | def do_encode 186 | ffmpeg(encoder.encoding_tasks.first) 187 | end 188 | 189 | # Shells out to ffmpeg and hits the given video with the parameters in the 190 | # given task. Will call itself recursively until all tasks in the encoder's 191 | # encoding_tasks are completed. 192 | # 193 | def ffmpeg(task) 194 | current_task_index = encoder.encoding_tasks.index(task) 195 | movie = FFMPEG::Movie.new(file_to_encode) 196 | encoding_operation = proc do 197 | movie.transcode(file_to_encode + task.output_file_suffix, task.command) 198 | end 199 | 200 | completion_callback = proc do |result| 201 | if task == encoder.encoding_tasks.last 202 | self.complete! 203 | else 204 | next_task_index = current_task_index + 1 205 | next_task = encoder.encoding_tasks[next_task_index] 206 | ffmpeg(next_task) 207 | end 208 | end 209 | 210 | EventMachine.defer(encoding_operation, completion_callback) 211 | end 212 | 213 | # Notifies a calling application that processing has completed by sending 214 | # a GET request to the video's callback_url. 215 | # 216 | def notify_complete 217 | EventMachine::HttpRequest.new(callback_url).get :timeout => 10 unless callback_url.nil? 218 | end 219 | 220 | # Downloads a video from a remote location via HTTP 221 | # 222 | def do_download 223 | FileUtils.rm(file_to_encode, :force => true) 224 | FileUtils.mkdir_p(File.dirname(file_to_encode)) 225 | http = EventMachine::HttpRequest.new(file).get :timeout => 10 226 | 227 | http.stream do |data| 228 | File.open(file_to_encode, 'a') {|f| f.write(data) } 229 | end 230 | 231 | http.callback do 232 | download_complete! 233 | end 234 | 235 | http.errback do 236 | download_error! 237 | end 238 | end 239 | 240 | # Returns false if the video is available via http 241 | # 242 | def local? 243 | return false if self.file =~ /^http:\/\// 244 | return true 245 | end 246 | 247 | # If the file is local, this just returns its location. If it's not local, 248 | # it builds a path to the file based on your config.yml + the video's id and 249 | # returns that. 250 | # 251 | def file_to_encode 252 | if local? 253 | return file 254 | else 255 | filename = File.basename(URI.parse(file).path) 256 | return File.join(EnigmaMachine.download_storage_path, self.id.to_s, filename) 257 | end 258 | end 259 | 260 | end 261 | 262 | -------------------------------------------------------------------------------- /lib/enigmamachine/public/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | Design by Free CSS Templates 3 | http://www.freecsstemplates.org 4 | Released for free under a Creative Commons Attribution 2.5 License 5 | */ 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | background: #FFFFFF url(images/bg01.jpg) repeat-x top left; 11 | font-size: 13px; 12 | color: #7F7F7F; 13 | } 14 | 15 | body, th, td, input, textarea, select, option { 16 | font-weight: normal; 17 | font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; 18 | } 19 | 20 | h1, h2, h3 { 21 | font-weight: normal; 22 | color: #484848; 23 | } 24 | 25 | h1 { 26 | text-transform: lowercase; 27 | letter-spacing: -1px; 28 | font-size: 24px; 29 | } 30 | 31 | h2 { 32 | text-transform: lowercase; 33 | letter-spacing: -1px; 34 | font-size: 24px; 35 | } 36 | 37 | h3 { 38 | font-size: 1em; 39 | } 40 | 41 | p, ul, ol { 42 | line-height: 200%; 43 | } 44 | 45 | blockquote { 46 | padding-left: 1em; 47 | } 48 | 49 | blockquote p, blockquote ul, blockquote ol { 50 | line-height: normal; 51 | font-style: italic; 52 | } 53 | 54 | a { 55 | color: #7ACE11; 56 | } 57 | 58 | a:hover { 59 | text-decoration: none; 60 | color: #7ACE11; 61 | } 62 | 63 | /* Header */ 64 | 65 | #header { 66 | width: 890px; 67 | height: 190px; 68 | margin: 0px auto; 69 | } 70 | 71 | /* Logo */ 72 | 73 | #logo { 74 | float: left; 75 | padding: 40px 0 0 0; 76 | } 77 | 78 | #logo h1 { 79 | margin: 0; 80 | text-transform: lowercase; 81 | letter-spacing: -2px; 82 | font-size: 3.6em; 83 | font-weight: normal; 84 | color: #FFFFFF; 85 | } 86 | 87 | #logo h1 a { 88 | padding-right: 20px; 89 | text-decoration: none; 90 | color: #FFFFFF; 91 | } 92 | 93 | #logo p { 94 | margin: -5px 0 0 0; 95 | text-transform: uppercase; 96 | font-size: 1.22em; 97 | letter-spacing: -1px; 98 | } 99 | 100 | #logo a { 101 | text-decoration: none; 102 | color: #FFFFFF; 103 | } 104 | 105 | /* Menu */ 106 | 107 | #menu { 108 | float: right; 109 | } 110 | 111 | #menu ul { 112 | margin: 0px; 113 | padding: 93px 0px 0px 0px; 114 | list-style: none; 115 | } 116 | 117 | #menu li { 118 | display: inline; 119 | } 120 | 121 | #menu a { 122 | display: block; 123 | float: left; 124 | margin-left: 20px; 125 | text-decoration: none; 126 | text-transform: lowercase; 127 | font-size: 1.36em; 128 | color: #FFFFFF; 129 | } 130 | 131 | #menu a:hover, .active a { 132 | border-bottom: 3px solid #FFFFFF; 133 | } 134 | 135 | 136 | /* Page */ 137 | 138 | #page { 139 | width: 890px; 140 | margin: 0 auto; 141 | } 142 | 143 | /* Content */ 144 | 145 | #content { 146 | float: left; 147 | width: 590px; 148 | } 149 | 150 | .post { 151 | padding: 20px 20px; 152 | background: url(images/bg04.jpg) no-repeat top left; 153 | } 154 | 155 | .title { 156 | margin: 0; 157 | border-bottom: 2px solid #484848; 158 | color: #484848; 159 | } 160 | 161 | .byline { 162 | margin: 0; 163 | color: #D79B00; 164 | } 165 | 166 | .meta { 167 | text-align: left; 168 | color: #646464; 169 | } 170 | 171 | .meta .more { 172 | padding-left: 20px; 173 | background: url(images/img03.gif) no-repeat left center; 174 | } 175 | 176 | .meta .comments { 177 | padding-left: 20px; 178 | background: url(images/img04.gif) no-repeat left center; 179 | } 180 | 181 | /* Sidebar */ 182 | 183 | #sidebar { 184 | float: right; 185 | width: 240px; 186 | } 187 | 188 | #sidebar ul { 189 | margin: 0; 190 | padding: 0; 191 | list-style: none; 192 | } 193 | 194 | #sidebar li { 195 | } 196 | 197 | #sidebar li ul { 198 | padding: 15px 0; 199 | } 200 | 201 | #sidebar li li { 202 | padding-left: 20px; 203 | border-bottom: 1px dotted #7B9418; 204 | } 205 | 206 | #sidebar h2 { 207 | margin: 0; 208 | padding: 5px 0 0 20px; 209 | background: url(images/img06.jpg) no-repeat left 80%; 210 | } 211 | 212 | #sidebar a { 213 | text-decoration: none; 214 | } 215 | 216 | #sidebar a:hover { 217 | } 218 | 219 | /* Footer */ 220 | 221 | #footer { 222 | clear: both; 223 | margin: 0px; 224 | height: 80px; 225 | background: #F2F2F2 url(images/bg02.jpg) repeat-x left top; 226 | } 227 | 228 | #footer p { 229 | padding: 20px 0; 230 | text-align: center; 231 | font-size: smaller; 232 | color: #717171; 233 | } 234 | -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/Enigma-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/Enigma-logo.jpg -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/bg01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/bg01.jpg -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/bg02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/bg02.jpg -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/bg03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/bg03.jpg -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/bg04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/bg04.jpg -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/img02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/img02.gif -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/img03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/img03.gif -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/img04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/img04.gif -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/img05.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/img05.gif -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/img06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/img06.jpg -------------------------------------------------------------------------------- /lib/enigmamachine/public/images/spacer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/lib/enigmamachine/public/images/spacer.gif -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoders/edit.erb: -------------------------------------------------------------------------------- 1 |
2 |

Edit resource

3 |
4 | <% unless @encoder.errors.empty? %> 5 |
6 |

Errors on Encoder

7 |
    8 | <% @encoder.errors.each do |error| %> 9 |
  • <%= error %>
  • 10 | <% end %> 11 |
12 |
13 | <% end %> 14 | 15 |
16 | 17 | <%= partial :"encoders/form" %> 18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoders/encoder.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= encoders_encoder.name %> - 3 | show 4 | edit 5 |
    6 | 7 | 8 |
    9 |
  • 10 | 11 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoders/encoding_task.erb: -------------------------------------------------------------------------------- 1 | <% task = encoders_encoding_task %> 2 |
    3 | <%= task.name %>: ffmpeg <%= task.command %> :: <%= task.output_file_suffix %> suffix :: edit 4 | 5 |
    6 | 7 | 8 |
    9 |
    10 | 11 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoders/form.erb: -------------------------------------------------------------------------------- 1 |

    2 | Name: 3 | 4 |

    5 | 6 | 7 | 8 |

    9 | cancel 10 |

    11 | 12 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoders/index.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Available encoders

    3 |
    4 | <% unless @encoders.empty? %> 5 |
      6 | <%= partial :'encoders/encoder', :collection => @encoders %> 7 |
    8 | <% end %> 9 |

    10 | New encoder 11 |

    12 |
    13 |
    14 | 15 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoders/new.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Create a new encoder

    3 |
    4 | 5 | <% unless @encoder.errors.empty? %> 6 |
    7 |

    Errors on Encoder

    8 |
      9 | <% @encoder.errors.each do |error| %> 10 |
    • 11 | <%= error %> 12 |
    • 13 | <% end %> 14 |
    15 |
    16 | <% end %> 17 | 18 |
    19 | <%= partial :"encoders/form" %> 20 |
    21 |
    22 |
    23 | 24 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoders/show.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Encoder: <%= @encoder.name %>

    3 |
    4 |

    5 | Encoding tasks that this encoder performs: 6 |

    7 | <% if @encoder.encoding_tasks.empty? %> 8 | None. 9 | <% end %> 10 | <%= partial(:"encoders/encoding_task", :collection => @encoder.encoding_tasks) %> 11 | 12 |

    13 | Add a new task to 14 | this encoder. 15 |

    16 | 17 |

    18 | Back to the list of available encoders. 19 |

    20 |
    21 |
    22 | 23 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoding_tasks/edit.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Edit an encoding task

    3 |
    4 | 5 | <% unless @encoding_task.errors.empty? %> 6 |
    7 |

    Errors on Encoding Task

    8 |
      9 | <% @encoding_task.errors.each do |error| %> 10 |
    • <%= error %>
    • 11 | <% end %> 12 |
    13 |
    14 | <% end %> 15 | 16 |
    19 | 20 | <%= partial(:'encoding_tasks/form') %> 21 |
    22 | 23 |
    24 |
    25 | 26 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoding_tasks/form.erb: -------------------------------------------------------------------------------- 1 |

    2 | Note: you can read about your options in the 3 | ffmpeg documentation 4 |

    5 |

    6 | Name: e.g. encode a 320x240 flv at 25 frames per second 7 |
    8 | 9 |

    10 | 11 |

    12 | Output file suffix: e.g. .flv 13 |
    14 | 15 |

    16 | 17 |

    18 | Encoding command: e.g. -ab 128 -ar 22050 -b 500000 -r 25 -s 320x240
    19 | 20 |

    21 | 22 | 23 |

    24 | cancel 25 |

    26 | 27 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/encoding_tasks/new.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    New encoding task

    3 |
    4 | 5 | <% unless @encoding_task.errors.empty? %> 6 |
    7 |

    Errors on Encoding Task

    8 |
      9 | <% @encoding_task.errors.each do |error| %> 10 |
    • <%= error %>
    • 11 | <% end %> 12 |
    13 |
    14 | <% end %> 15 | 16 |

    Create a new encoding task for this encoder

    17 |
    18 | <%= partial(:"encoding_tasks/form")%> 19 |
    20 | 21 |
    22 |
    23 | 24 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/index.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Your own enigma machine

    3 |
    4 | <% unless @videos.empty? %> 5 |

    Some recently added videos:

    6 |
      7 | <%= partial :'videos/video', :collection => @videos %> 8 |
    9 | <% end %> 10 |
    11 |
    12 | 13 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | Enigma Admin 17 | 18 | 19 | 20 | 21 | 22 | 23 | 37 | 38 | 39 |
    40 | 41 |
    42 | 43 | <%= yield %> 44 | 45 |
    46 | 47 | 48 | 57 | 58 |
    59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/videos/form.erb: -------------------------------------------------------------------------------- 1 |

    2 | File location: 3 |
    4 | 5 |

    6 | 11 | 12 |

    13 | Callback url: 14 |
    15 | 16 |

    17 | 18 | 19 |

    20 | cancel 21 |

    22 | 23 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/videos/index.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Videos

    3 |
    4 | 5 |

    6 | New video 7 |

    8 | 9 | <% unless @encoding_videos.empty? %> 10 |

    Currently encoding

    11 |
      12 | <%= partial :"videos/video", :collection => @encoding_videos %> 13 |
    14 | <% end %> 15 | 16 | <% unless @unencoded_videos.empty? %> 17 |

    Waiting to encode

    18 |
      19 | <%= partial :"videos/video", :collection => @unencoded_videos %> 20 |
    21 | <% end %> 22 | 23 | <% unless @completed_videos.empty? %> 24 |

    Completed

    25 |
      26 | <%= partial :"videos/video", :collection => @completed_videos %> 27 |
    28 | <% end %> 29 | 30 | <% unless @videos_with_errors.empty? %> 31 |

    Failed to encode

    32 |
      33 | <%= partial :"videos/video", :collection => @videos_with_errors %> 34 |
    35 | <% end %> 36 | 37 | <% unless @downloading_videos.empty? %> 38 |

    Failed to encode

    39 |
      40 | <%= partial :"videos/video", :collection => @downloading_videos %> 41 |
    42 | <% end %> 43 | 44 | <% unless @waiting_for_download_videos.empty? %> 45 |

    Failed to encode

    46 |
      47 | <%= partial :"videos/video", :collection => @waiting_for_download_videos %> 48 |
    49 | <% end %> 50 | 51 |
    52 |
    53 | 54 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/videos/new.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Create a new video

    3 |
    4 | <% unless @video.errors.empty? %> 5 |
    6 |

    Errors on video

    7 |
      8 | <% @video.errors.each do |error| %> 9 |
    • 10 | <%= error %> 11 |
    • 12 | <% end %> 13 |
    14 |
    15 | <% end %> 16 | 17 |
    18 | <%= partial :"videos/form" %> 19 |
    20 |
    21 |
    22 | 23 | -------------------------------------------------------------------------------- /lib/enigmamachine/views/videos/video.erb: -------------------------------------------------------------------------------- 1 | <% video = videos_video %> 2 |
  • 3 | <%= video.state %> - <%=h video.file %> - 4 | created at <%=h video.created_at.strftime("%m/%d/%Y at %H:%M:%S")%> 5 |
    6 | 7 | 8 |
    9 |
    10 |
  • 11 | 12 | -------------------------------------------------------------------------------- /lib/ext/array_ext.rb: -------------------------------------------------------------------------------- 1 | 2 | # Monkey patches 3 | class Array 4 | def extract_options! 5 | last.is_a?(::Hash) ? pop : {} 6 | end 7 | end 8 | 9 | -------------------------------------------------------------------------------- /lib/ext/partials.rb: -------------------------------------------------------------------------------- 1 | # An implementation of partials for Sinatra. 2 | # 3 | # Can be used like: <%= partial(:foo) %> 4 | # 5 | # Unlike Rails partials, the .erb files for these partials do not start with a 6 | # leading underscore, i.e. the file name should be foo.rb, not _foo.rb. 7 | # 8 | # Liberated from: 9 | # http://github.com/cschneid/irclogger/blob/master/lib/partials.rb 10 | # 11 | module Sinatra 12 | module Partials 13 | def partial(template, *args) 14 | options = args.extract_options! 15 | options.merge!(:layout => false) 16 | if collection = options.delete(:collection) then 17 | collection.inject([]) do |buffer, member| 18 | buffer << erb(template, options.merge(:layout => 19 | false, :locals => {template.to_s.gsub!("/", "_").to_sym => member})) 20 | # check with template.to_s.split("/").last.to_sym => member 21 | end.join("\n") 22 | else 23 | erb(template, options) 24 | end 25 | end 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /lib/generators/config.yml: -------------------------------------------------------------------------------- 1 | username: admin 2 | password: admin 3 | threads: 1 4 | enable_http_downloads: false 5 | download_storage_path: /path/to/where/downloads/should/end/up 6 | 7 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra/base' 3 | require 'rack/test' 4 | require 'base64' 5 | require 'machinist/data_mapper' 6 | require 'faker' 7 | require 'sham' 8 | require File.dirname(__FILE__) + '/../lib/enigmamachine' 9 | require File.expand_path(File.dirname(__FILE__) + "/support/blueprints") 10 | 11 | 12 | ENV['RACK_ENV'] = 'test' 13 | 14 | module TestHelper 15 | 16 | def app 17 | EnigmaMachine.new 18 | end 19 | 20 | def body 21 | last_response.body 22 | end 23 | 24 | def status 25 | last_response.status 26 | end 27 | 28 | include Rack::Test::Methods 29 | 30 | end 31 | 32 | require 'test/unit' 33 | require 'shoulda' 34 | 35 | Test::Unit::TestCase.send(:include, TestHelper) 36 | 37 | def basic_auth_creds(username = 'admin', password = 'admin') 38 | {'HTTP_AUTHORIZATION'=> encode_credentials(username, password)} 39 | end 40 | 41 | def encode_credentials(username, password) 42 | "Basic " + Base64.encode64("#{username}:#{password}") 43 | end 44 | 45 | def http_file_location 46 | "http://foo.org/bar/blah.wmv" 47 | end 48 | 49 | -------------------------------------------------------------------------------- /test/support/afile.mpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurechimp/enigmamachine/64aa9335a4f8c6f39030be870e9ce44f8b0dc1d9/test/support/afile.mpg -------------------------------------------------------------------------------- /test/support/blueprints.rb: -------------------------------------------------------------------------------- 1 | require 'machinist/data_mapper' 2 | require 'faker' 3 | require 'sham' 4 | 5 | 6 | Encoder.blueprint do 7 | name { Faker::Name.name } 8 | end 9 | 10 | EncodingTask.blueprint do 11 | name { "320x240 flv"} 12 | output_file_suffix { ".foo.flv" } 13 | command { "-ss 00:00:02 -t 00:00:01 -vcodec mjpeg -vframes 1 -an -f rawvideo -s 180x136"} 14 | end 15 | 16 | EncodingTask.blueprint(:with_encoder) do 17 | name { "320x240 flv"} 18 | output_file_suffix { ".foo.flv" } 19 | command { "-ss 00:00:02 -t 00:00:01 -vcodec mjpeg -vframes 1 -an -f rawvideo -s 180x136"} 20 | encoder 21 | end 22 | 23 | Video.blueprint do 24 | encoder 25 | file { File.dirname(__FILE__) + "/afile.mpg" } 26 | state { "unencoded" } 27 | created_at DateTime.now 28 | updated_at DateTime.now 29 | end 30 | 31 | Video.blueprint(:http) do 32 | file { http_file_location } 33 | end 34 | 35 | Video.blueprint(:with_callback) do 36 | callback_url { "http://example.com/call/back/id" } 37 | end 38 | 39 | -------------------------------------------------------------------------------- /test/test_encoder.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestEncoder < Test::Unit::TestCase 4 | 5 | context "An Encoder instance" do 6 | 7 | setup do 8 | @encoder = Encoder.make 9 | end 10 | 11 | should "be invalid without a name" do 12 | @encoder.name = "" 13 | assert !@encoder.valid? 14 | end 15 | 16 | should "have an encoding_tasks association" do 17 | assert @encoder.respond_to? "encoding_tasks" 18 | end 19 | 20 | should "allow encoding_tasks to be added" do 21 | encoder = Encoder.make_unsaved 22 | task = EncodingTask.make_unsaved 23 | assert_nothing_raised do 24 | encoder.encoding_tasks << task 25 | end 26 | end 27 | 28 | context "hitting the ffmpeg method" do 29 | context "for an encoder with 1 task" do 30 | setup do 31 | @video = Video.make 32 | task = EncodingTask.make 33 | @video.encoder.encoding_tasks << task 34 | @video.encoder.save 35 | @video.encode! 36 | @id = @video.id 37 | sleep 1 38 | end 39 | 40 | should "create the file specified in the encoder's first encoding task" do 41 | suffix = @video.encoder.encoding_tasks.first.output_file_suffix 42 | file = @video.file + suffix 43 | assert File.exist? file 44 | end 45 | 46 | should_eventually "mark the video as complete" do 47 | sleep 5 48 | video = Video.get(@id) 49 | assert_equal "complete", video.state 50 | end 51 | 52 | end 53 | 54 | context "for an encoder with 2 tasks" do 55 | setup do 56 | video = Video.make 57 | task = EncodingTask.make 58 | 2.times { video.encoder.encoding_tasks << EncodingTask.make } 59 | video.encoder.save 60 | video.encode! 61 | @id = video.id 62 | end 63 | 64 | should_eventually "mark the video as complete" do 65 | sleep 1 66 | video = Video.get(@id) 67 | assert_equal "complete", video.state 68 | end 69 | end 70 | 71 | end 72 | end 73 | end 74 | 75 | -------------------------------------------------------------------------------- /test/test_encoding_queue.rb: -------------------------------------------------------------------------------- 1 | #require File.dirname(__FILE__) + '/helper' 2 | 3 | #class TestEncoderQueue < Test::Unit::TestCase 4 | 5 | ## context "the encode_next_video method" do 6 | ## setup do 7 | ## destroy_all_videos 8 | ## @video = Video.make 9 | ## @task = EncodingTask.make(:with_encoder) 10 | ## @video.encoder = @task.encoder 11 | ## @video.save 12 | ### sleep 2 13 | ## @queue = EncodingQueue.new 14 | ## @queue.encode_next_video 15 | ## end 16 | 17 | ## should_eventually "exist" do 18 | ## assert @queue.respond_to? "encode_next_video" 19 | ## end 20 | 21 | ## should_eventually "start encoding the video" do 22 | ## v = Video.get(@video.id) 23 | ## assert_equal "encoding", v.state 24 | ## end 25 | ## end 26 | 27 | ## private 28 | 29 | ## def destroy_all_videos 30 | ## Video.all.each { |v| v.destroy! } 31 | ## end 32 | 33 | #end 34 | 35 | -------------------------------------------------------------------------------- /test/test_enigmamachine.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestEnigmamachine < Test::Unit::TestCase 4 | 5 | context "on GET to / without credentials" do 6 | setup do 7 | get '/' 8 | end 9 | 10 | should "respond with security error" do 11 | assert !last_response.ok? 12 | assert_equal 401, status 13 | end 14 | end 15 | 16 | context "on GET to / with credentials" do 17 | setup do 18 | get '/', {}, basic_auth_creds 19 | end 20 | 21 | should "work" do 22 | assert last_response.ok? 23 | end 24 | 25 | context "when there is one Video" do 26 | setup do 27 | Video.make 28 | get '/', {}, basic_auth_creds 29 | end 30 | 31 | should "work" do 32 | assert last_response.ok? 33 | end 34 | end 35 | end 36 | 37 | context "on GET to /encoders" do 38 | context "without credentials" do 39 | setup do 40 | get '/encoders' 41 | end 42 | 43 | should "respond with security error" do 44 | assert !last_response.ok? 45 | assert_equal 401, status 46 | end 47 | end 48 | 49 | context "with credentials" do 50 | setup do 51 | get '/encoders', {}, basic_auth_creds 52 | end 53 | 54 | should "work" do 55 | assert last_response.ok? 56 | end 57 | 58 | context "when some encoders exist" do 59 | setup do 60 | 2.times { Encoder.make } 61 | end 62 | 63 | should "still work" do 64 | assert last_response.ok? 65 | end 66 | end 67 | end 68 | end 69 | 70 | context "on GET to /encoder/:id" do 71 | context "without credentials" do 72 | setup do 73 | get "/encoder/#{Encoder.first.id}" 74 | end 75 | 76 | should "respond with security error" do 77 | assert !last_response.ok? 78 | assert_equal 401, status 79 | end 80 | end 81 | 82 | context "with credentials" do 83 | setup do 84 | get "/encoders/#{Encoder.first.id}", {:id => Encoder.first.id}, basic_auth_creds 85 | end 86 | 87 | should "work" do 88 | assert last_response.ok? 89 | end 90 | end 91 | end 92 | 93 | context "on GET to /encoders/new" do 94 | context "without credentials" do 95 | setup do 96 | get '/encoders/new' 97 | end 98 | 99 | should "respond with security error" do 100 | assert !last_response.ok? 101 | assert_equal 401, status 102 | end 103 | end 104 | 105 | context "with credentials" do 106 | setup do 107 | get '/encoders/new', {}, basic_auth_creds 108 | end 109 | 110 | should "work" do 111 | assert last_response.ok? 112 | end 113 | end 114 | end 115 | 116 | context "on GET to /encoders/edit" do 117 | setup do 118 | @encoder = Encoder.make 119 | end 120 | 121 | context "without credentials" do 122 | setup do 123 | get "/encoders/#{@encoder.id}/edit" 124 | end 125 | 126 | should "respond with security error" do 127 | assert !last_response.ok? 128 | assert_equal 401, status 129 | end 130 | end 131 | 132 | context "with credentials" do 133 | setup do 134 | get "/encoders/#{@encoder.id}/edit", {}, basic_auth_creds 135 | end 136 | 137 | should "work" do 138 | assert last_response.ok? 139 | end 140 | end 141 | end 142 | 143 | context "on POST to /encoders" do 144 | context "without credentials" do 145 | setup do 146 | post "/encoders", Encoder.plan 147 | end 148 | 149 | should "respond with security error" do 150 | assert !last_response.ok? 151 | assert_equal 401, status 152 | end 153 | end 154 | 155 | context "with credentials" do 156 | 157 | context "and valid Encoder params" do 158 | setup do 159 | @num_encoders = Encoder.count 160 | post "/encoders", {:encoder => Encoder.plan}, basic_auth_creds 161 | follow_redirect! 162 | end 163 | 164 | should "create an Encoder object" do 165 | assert_equal "http://example.org/encoders/#{Encoder.last.id}", last_request.url 166 | assert_equal @num_encoders + 1, Encoder.count 167 | end 168 | end 169 | 170 | context "and invalid Encoder params" do 171 | setup do 172 | @num_encoders = Encoder.count 173 | post "/encoders", {:encoder => Encoder.plan.merge(:name => "")}, basic_auth_creds 174 | end 175 | 176 | should "redisplay the form" do 177 | assert_equal "http://example.org/encoders", last_request.url 178 | end 179 | 180 | should "not create an Encoder object" do 181 | assert_equal @num_encoders, Encoder.count 182 | end 183 | end 184 | end 185 | end 186 | 187 | context "on PUT to /encoders/:id" do 188 | context "without credentials" do 189 | setup do 190 | put "/encoders/#{Encoder.first.id}", {:encoder => Encoder.plan} 191 | end 192 | 193 | should "respond with security error" do 194 | assert !last_response.ok? 195 | assert_equal 401, status 196 | end 197 | end 198 | 199 | context "with credentials" do 200 | context "and valid encoder params" do 201 | setup do 202 | put "/encoders/#{Encoder.first.id}", {:id => Encoder.first.id, :encoder => Encoder.plan}, basic_auth_creds 203 | @num_encoders = Encoder.count 204 | follow_redirect! 205 | end 206 | 207 | should "work" do 208 | assert_equal "http://example.org/encoders", last_request.url 209 | end 210 | 211 | should "not create a new Encoder object" do 212 | assert_equal @num_encoders, Encoder.count 213 | end 214 | end 215 | 216 | context "and invalid encoder params" do 217 | setup do 218 | put "/encoders/#{Encoder.first.id}", { 219 | :id => Encoder.first.id, 220 | :encoder => Encoder.plan.merge(:name => "")}, basic_auth_creds 221 | @num_encoders = Encoder.count 222 | end 223 | 224 | should "redisplay the edit form" do 225 | assert_equal "http://example.org/encoders/#{Encoder.first.id}", last_request.url 226 | end 227 | 228 | should "not create a new Encoder object" do 229 | assert_equal @num_encoders, Encoder.count 230 | end 231 | end 232 | 233 | end 234 | end 235 | 236 | context "on DELETE to /encoders" do 237 | context "without credentials" do 238 | setup do 239 | @encoder = Encoder.make 240 | delete "/encoders/#{@encoder.id}", {:id => @encoder.id} 241 | end 242 | 243 | should "respond with security error" do 244 | assert !last_response.ok? 245 | assert_equal 401, status 246 | end 247 | end 248 | 249 | context "with credentials" do 250 | setup do 251 | @encoder = Encoder.make 252 | @num_encoders = Encoder.count 253 | delete "/encoders/#{@encoder.id}", {:id => @encoder.id}, basic_auth_creds 254 | follow_redirect! 255 | end 256 | 257 | should "destroy the encoder" do 258 | assert_equal @num_encoders - 1, Encoder.count 259 | end 260 | 261 | should "redirect to the encoder list page" do 262 | assert_equal "http://example.org/encoders", last_request.url 263 | end 264 | end 265 | end 266 | 267 | context "on GET to /encoding_tasks/new/:encoder_id" do 268 | context "without credentials" do 269 | setup do 270 | get "/encoding_tasks/new/#{Encoder.first.id}" 271 | end 272 | 273 | should "respond with security error" do 274 | assert !last_response.ok? 275 | assert_equal 401, status 276 | end 277 | end 278 | 279 | context "with credentials" do 280 | setup do 281 | get "/encoding_tasks/new/#{Encoder.first.id}", {}, basic_auth_creds 282 | end 283 | 284 | should "work" do 285 | assert last_response.ok? 286 | end 287 | end 288 | end 289 | 290 | 291 | context "on GET to /encoding_tasks/:id/edit" do 292 | context "without credentials" do 293 | setup do 294 | get "/encoding_tasks/#{Encoder.first.id}/edit" 295 | end 296 | 297 | should "respond with security error" do 298 | assert !last_response.ok? 299 | assert_equal 401, status 300 | end 301 | end 302 | 303 | context "with credentials" do 304 | setup do 305 | get "/encoding_tasks/#{Encoder.first.id}/edit", {}, basic_auth_creds 306 | end 307 | 308 | should "work" do 309 | assert last_response.ok? 310 | end 311 | end 312 | end 313 | 314 | 315 | context "on POST to /encoding_tasks/:encoder_id" do 316 | context "without credentials" do 317 | setup do 318 | post "/encoding_tasks/#{Encoder.first.id}" 319 | end 320 | 321 | should "respond with security error" do 322 | assert !last_response.ok? 323 | assert_equal 401, status 324 | end 325 | end 326 | 327 | context "with credentials" do 328 | context "and valid EncodingTask params" do 329 | setup do 330 | @num_tasks = EncodingTask.count 331 | post "/encoding_tasks/#{Encoder.first.id}", {:encoding_task => EncodingTask.plan}, basic_auth_creds 332 | follow_redirect! 333 | end 334 | 335 | should "create a new encoding task" do 336 | assert_equal @num_tasks + 1, EncodingTask.count 337 | end 338 | 339 | should "redirect to parent encoder show page" do 340 | assert_equal "http://example.org/encoders/#{Encoder.first.id}", last_request.url 341 | end 342 | end 343 | 344 | context "and invalid EncodingTask params" do 345 | setup do 346 | @num_tasks = EncodingTask.count 347 | post "/encoding_tasks/#{Encoder.first.id}", { 348 | :encoding_task => EncodingTask.plan.merge(:name => "")}, basic_auth_creds 349 | end 350 | 351 | should "not create a new encoding task" do 352 | assert_equal @num_tasks, EncodingTask.count 353 | end 354 | 355 | should "redisplay the EncodingTask form" do 356 | assert last_response.ok? 357 | end 358 | end 359 | end 360 | end 361 | 362 | context "on PUT to /encoding_tasks/:id" do 363 | context "without credentials" do 364 | setup do 365 | put "/encoding_tasks/#{Encoder.first.id}" 366 | end 367 | 368 | should "respond with security error" do 369 | assert !last_response.ok? 370 | assert_equal 401, status 371 | end 372 | end 373 | 374 | context "with credentials" do 375 | context "and valid EncodingTask params" do 376 | setup do 377 | @encoding_task = EncodingTask.make(:with_encoder) 378 | @num_tasks = EncodingTask.count 379 | put "/encoding_tasks/#{@encoding_task.id}", {:id => @encoding_task.id, :encoding_task => EncodingTask.plan}, basic_auth_creds 380 | follow_redirect! 381 | end 382 | 383 | should "not create a new encoding task" do 384 | assert_equal @num_tasks, EncodingTask.count 385 | end 386 | 387 | should "redirect to parent encoder show page" do 388 | assert_equal "http://example.org/encoders/#{@encoding_task.encoder.id}", last_request.url 389 | end 390 | end 391 | 392 | context "and invalid EncodingTask params" do 393 | setup do 394 | @encoding_task = EncodingTask.make(:with_encoder) 395 | @num_tasks = EncodingTask.count 396 | put "/encoding_tasks/#{@encoding_task.id}", { 397 | :encoding_task => EncodingTask.plan.merge(:name => "")}, basic_auth_creds 398 | end 399 | 400 | should "not create a new encoding task" do 401 | assert_equal @num_tasks, EncodingTask.count 402 | end 403 | 404 | should "redisplay the EncodingTask form" do 405 | assert last_response.ok? 406 | end 407 | end 408 | end 409 | end 410 | 411 | context "on DELETE to /encoding_tasks/:id" do 412 | context "without credentials" do 413 | setup do 414 | @task = EncodingTask.make 415 | delete "/encoding_tasks/#{@task.id}" 416 | end 417 | 418 | should "respond with security error" do 419 | assert !last_response.ok? 420 | assert_equal 401, status 421 | end 422 | end 423 | 424 | context "with credentials" do 425 | setup do 426 | @task = EncodingTask.make 427 | @num_tasks = EncodingTask.count 428 | delete "/encoding_tasks/#{@task.id}", {:id => @task.id}, basic_auth_creds 429 | end 430 | 431 | should "destroy the task" do 432 | assert_equal @num_tasks - 1, EncodingTask.count 433 | end 434 | 435 | end 436 | end 437 | 438 | 439 | context "on GET to /videos" do 440 | context "without credentials" do 441 | setup do 442 | get "/videos" 443 | end 444 | 445 | should "respond with security error" do 446 | assert !last_response.ok? 447 | assert_equal 401, status 448 | end 449 | end 450 | 451 | context "with credentials" do 452 | setup do 453 | get "/videos", {}, basic_auth_creds 454 | end 455 | 456 | should "work" do 457 | assert last_response.ok? 458 | end 459 | end 460 | end 461 | 462 | context "on GET to /videos/new" do 463 | context "without credentials" do 464 | setup do 465 | get "/videos/new" 466 | end 467 | 468 | should "respond with security error" do 469 | assert !last_response.ok? 470 | assert_equal 401, status 471 | end 472 | end 473 | end 474 | 475 | context "with credentials" do 476 | setup do 477 | get "/videos/new", {}, basic_auth_creds 478 | end 479 | 480 | should "work" do 481 | assert last_response.ok? 482 | end 483 | end 484 | 485 | context "on POST to /videos" do 486 | context "without credentials" do 487 | setup do 488 | post "/videos", {:video => Video.plan} 489 | end 490 | 491 | should "respond with security error" do 492 | assert !last_response.ok? 493 | assert_equal 401, status 494 | end 495 | end 496 | 497 | context "with credentials" do 498 | context "and valid video params" do 499 | setup do 500 | @num_videos = Video.count 501 | post "/videos", { 502 | :video => Video.plan, 503 | :encoder_id => Encoder.make.id}, basic_auth_creds 504 | follow_redirect! 505 | end 506 | 507 | should "create a video" do 508 | assert_equal @num_videos + 1, Video.count 509 | end 510 | 511 | should "redirect to /videos" do 512 | assert_equal "http://example.org/videos", last_request.url 513 | end 514 | end 515 | 516 | context "when video[callback_url] is set" do 517 | setup do 518 | @num_videos = Video.count 519 | post "/videos", { 520 | :video => Video.plan(:with_callback), 521 | :encoder_id => Encoder.make.id}, basic_auth_creds 522 | follow_redirect! 523 | end 524 | 525 | should "create a video" do 526 | assert_equal @num_videos + 1, Video.count 527 | end 528 | 529 | should "redirect to /videos" do 530 | assert_equal "http://example.org/videos", last_request.url 531 | end 532 | end 533 | 534 | context "and invalid video params" do 535 | setup do 536 | @num_videos = Video.count 537 | post "/videos", { 538 | :video => Video.plan.merge(:file => ""), 539 | :encoder_id => Encoder.make.id}, basic_auth_creds 540 | end 541 | 542 | should "not create a new video" do 543 | assert_equal @num_videos, Video.count 544 | end 545 | 546 | should "redisplay the video form" do 547 | assert last_response.ok? 548 | end 549 | end 550 | end 551 | end 552 | 553 | end 554 | 555 | -------------------------------------------------------------------------------- /test/test_video.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class TestVideo < Test::Unit::TestCase 4 | 5 | 6 | context "A Video instance" do 7 | 8 | should "be invalid with a bad file path" do 9 | video = Video.make 10 | video.file = "" 11 | assert(!video.valid?, "must not be empty") 12 | video.file = nil 13 | assert(!video.valid?, "must not be nil") 14 | video.file = "/fdfdf/sfdsdfsd/fse.gfr" 15 | assert(!video.valid?, "must exist") 16 | video.file = File.dirname(__FILE__) 17 | assert(!video.valid?, "must not be a directory") 18 | video.file = __FILE__ 19 | assert(!video.valid?, "must be media file") 20 | end 21 | 22 | should "be valid without a callback_url" do 23 | video = Video.make 24 | video.callback_url = "" 25 | assert video.valid? 26 | video.callback_url = nil 27 | assert video.valid? 28 | end 29 | 30 | should "be valid with a callback_url" do 31 | video = Video.make 32 | video.callback_url = "blah" 33 | assert video.valid? 34 | end 35 | 36 | should "be valid with a correct file path" do 37 | video = ::Video.make 38 | assert video.valid? 39 | end 40 | 41 | should "belong to an Encoder" do 42 | v = Video.make_unsaved 43 | assert v.respond_to? "encoder" 44 | end 45 | 46 | should "allow itself to be associated with an Encoder" do 47 | v = Video.make_unsaved 48 | e = Encoder.make_unsaved 49 | assert_nothing_raised do 50 | v.encoder = e 51 | end 52 | end 53 | 54 | context "on a local filesystem" do 55 | setup do 56 | @video = Video.make 57 | end 58 | 59 | should "have an initial state of 'unencoded'" do 60 | assert_equal("unencoded", @video.state) 61 | end 62 | 63 | should "transition to state 'encoding' on 'encode!' command" do 64 | @video.encode! 65 | assert_equal("encoding", @video.state) 66 | end 67 | 68 | should "transition to state 'unencoded' on 'reset!' command" do 69 | @video.encode! 70 | @video.reset! 71 | assert_equal("unencoded", @video.state) 72 | end 73 | 74 | end 75 | 76 | context "available via http" do 77 | setup do 78 | @video = Video.make(:http) 79 | end 80 | 81 | should "have an initial state of 'waiting_for_download'" do 82 | assert_equal("waiting_for_download", @video.state) 83 | end 84 | 85 | context "for the download! event" do 86 | setup do 87 | EventMachine.run do 88 | EventMachine::MockHttpRequest.use { 89 | EventMachine::HttpRequest.register_file(http_file_location, :get, '/home/dave/workspace/enigmamachine/test/support/afile.mpg') 90 | } 91 | @video.download! 92 | EventMachine.stop 93 | end 94 | end 95 | 96 | should_eventually "transition to state 'downloading'" do 97 | assert_equal("downloading", @video.state) 98 | end 99 | 100 | should_eventually "hit the download URL once" do 101 | EventMachine::MockHttpRequest.activate! 102 | assert_equal(1, EventMachine::HttpRequest.count(http_file_location, :get)) 103 | EventMachine::MockHttpRequest.deactivate! 104 | end 105 | 106 | end 107 | end 108 | end 109 | 110 | context "The Video class" do 111 | 112 | should "be able to grab all unencoded videos" do 113 | assert Video.respond_to? "unencoded" 114 | end 115 | 116 | context "when one Video exists" do 117 | setup do 118 | clear_videos 119 | Video.make(:state => "unencoded") 120 | end 121 | 122 | should "have one unencoded video" do 123 | assert_equal 1, Video.unencoded.count 124 | end 125 | end 126 | 127 | context "when two Videos exist" do 128 | setup do 129 | clear_videos 130 | 2.times { Video.make } 131 | end 132 | 133 | should "have two unencoded videos" do 134 | assert_equal 2, Video.unencoded.count 135 | end 136 | end 137 | 138 | context "deleting videos" do 139 | setup do 140 | clear_videos 141 | 5.times { Video.make } 142 | end 143 | 144 | should "delete an unencoded video" do 145 | count = Video.unencoded.count 146 | 2.times { Video.unencoded.first.destroy } 147 | assert_equal count - 2, Video.unencoded.count 148 | end 149 | 150 | should "delete a completed video" do 151 | 3.times { Video.unencoded.first.update(:state => "complete") } 152 | count = Video.complete.count 153 | 2.times { Video.complete.first.destroy } 154 | assert_equal count - 2, Video.complete.count 155 | end 156 | 157 | should "delete videos with errors" do 158 | 3.times { Video.unencoded.first.update(:state => "encode_error") } 159 | count = Video.with_encode_errors.count 160 | 2.times { Video.with_encode_errors.first.destroy } 161 | assert_equal count - 2, Video.with_encode_errors.count 162 | end 163 | 164 | should "not delete an encoding video" do 165 | 3.times { Video.unencoded.first.update(:state => "encoding") } 166 | count = Video.encoding.count 167 | 2.times { Video.encoding.first.destroy } 168 | assert_equal count, Video.encoding.count 169 | end 170 | 171 | should "allow force destroy of an encoding video" do 172 | 3.times { Video.unencoded.first.update(:state => "encoding") } 173 | count = Video.encoding.count 174 | 2.times { Video.encoding.first.destroy! } 175 | assert_equal count - 2, Video.encoding.count 176 | end 177 | 178 | end 179 | 180 | should "be able to grab all completed videos" do 181 | assert Video.respond_to? "complete" 182 | end 183 | 184 | should "be able to grab all videos with errors" do 185 | assert Video.respond_to? "with_encode_errors" 186 | end 187 | 188 | should "be able to grab all videos that are encoding" do 189 | assert Video.respond_to? "encoding" 190 | end 191 | 192 | should "be able to grab all videos that are not yet downloaded" do 193 | assert Video.respond_to? "waiting_for_download" 194 | end 195 | 196 | should "be able to grab all videos that are downloading" do 197 | assert Video.respond_to? "downloading" 198 | end 199 | 200 | end 201 | 202 | def clear_videos 203 | Video.all.each {|v| v.destroy! } 204 | end 205 | 206 | end 207 | 208 | --------------------------------------------------------------------------------