├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __init__.py ├── batch_generator.py ├── ddex ├── __init__.py ├── ddex.py ├── ddex_builder.py ├── deal.py ├── enum.py ├── file_metadata.py ├── message_header.py ├── party.py ├── release.py ├── release_builder.py ├── resource.py ├── tests │ ├── __init__.py │ ├── data.py │ ├── data_helper.py │ ├── resources │ │ ├── ddex-sample.xml │ │ ├── test.jpg │ │ ├── test.mp3 │ │ └── xsds │ │ │ ├── ddex.xsd │ │ │ ├── ddexC.xsd │ │ │ ├── iso3166a2.xsd │ │ │ ├── iso4217a.xsd │ │ │ ├── iso639a2.xsd │ │ │ └── release-notification.xsd │ ├── test_batch_generator.py │ ├── test_ddex.py │ ├── test_ddex_builder.py │ ├── test_deal.py │ ├── test_file_parser.py │ ├── test_id_generators.py │ ├── test_message_header.py │ ├── test_party.py │ ├── test_party_repository.py │ ├── test_release.py │ ├── test_release_builder.py │ ├── test_release_id.py │ ├── test_resource.py │ ├── test_resource_manager.py │ ├── test_validate.py │ └── test_validates_against_ddex_schema.py └── validate.py ├── deal_window.py ├── file_parser.py ├── inputs.py ├── metadata_form.py ├── party_repository.py ├── product_service.py ├── release_window.py ├── requirements.txt ├── res └── favicon.gif ├── resource_manager.py ├── run_tests.cmd ├── run_tests.sh ├── setup.py ├── tkinterutil.py └── unpackEgg.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.swp 3 | ddexui 4 | *.swn 5 | *.swo 6 | out 7 | PIL 8 | build 9 | EGG-INFO 10 | eggs.pth 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: "3.3" 3 | script: nosetests 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., [http://fsf.org/] 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/willm/DDEXUI.png?branch=master)](https://travis-ci.org/willm/DDEXUI) 2 | 3 | #DDEXUI 4 | 5 | DDEXUI provides a user interface for supplying digital music in a [ddex](http://ddex.net/) compliant way. It aims to abstract the complexities of ddex for ease of use by smaller independent labels and artists. 6 | 7 | DDEXUI is currently in development, but parts of the ddex spec have already been covered. 8 | 9 | requires python3.3. 10 | 11 | To start the program run: 12 | 13 | ` 14 | python metadata_form.py 15 | ` 16 | 17 | ### Installing dependencies 18 | 19 | #### Linux 20 | 21 | Assuming you have pip3 installed: 22 | 23 | ` 24 | pip install -r requirements.txt 25 | ` 26 | 27 | #### Windows 28 | 29 | install pip3: 30 | 31 | ``` 32 | pip install -r requirements.txt 33 | 34 | #install of pillow and lxml will fail. I haven't yet managed to install lxml on windows, but it is only a dependency on the tests 35 | 36 | easy_install Pillow 37 | ``` 38 | 39 | ### Packaging 40 | 41 | DDEXUI uses the awesome [cx_freeze](http://cx-freeze.sourceforge.net/) library to package itself into a windows executable that can be run without python being installed. To package the current version of the code to an exe, install cx_freeze and run: 42 | 43 | ``` 44 | python unpackEgg.py Pillow 45 | python setup.py build 46 | ``` 47 | 48 | this will create the executable version of the program in: 49 | 50 | ``` 51 | build/exe.win-XX/metadata_form.exe 52 | ``` 53 | 54 | To run the exe, users will need to install the Microsoft Visual C++ Redistributable Package that matches the processor architecture [32bit](http://www.microsoft.com/en-gb/download/details.aspx?id=5555) [64bit](http://www.microsoft.com/en-us/download/details.aspx?id=14632) 55 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willm/DDEXUI/c10e879cf1a32bcd26005f215f0693d3015b9f29/__init__.py -------------------------------------------------------------------------------- /batch_generator.py: -------------------------------------------------------------------------------- 1 | from os import makedirs 2 | from os import path 3 | from DDEXUI.ddex.ddex import generate_batch_id 4 | 5 | class BatchGenerator: 6 | def __init__(self, root_folder, batch_id): 7 | self._batch_id = batch_id 8 | self._root_folder = root_folder 9 | 10 | def generate(self, builders): 11 | batch_path = path.join(self._root_folder, self._batch_id) 12 | for builder in builders: 13 | ddex = builder.build() 14 | product_path = path.join(batch_path, builder.get_upc()) 15 | makedirs(product_path, exist_ok=True) 16 | ddex.write(path.join(product_path, builder.get_upc() + ".xml")) 17 | -------------------------------------------------------------------------------- /ddex/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willm/DDEXUI/c10e879cf1a32bcd26005f215f0693d3015b9f29/ddex/__init__.py -------------------------------------------------------------------------------- /ddex/ddex.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as ET 2 | import datetime 3 | from DDEXUI.ddex.message_header import MessageHeader 4 | from DDEXUI.ddex.release import ReleaseIdType 5 | 6 | def generate_batch_id(now=datetime.datetime.now): 7 | return now().strftime("%Y%m%d%H%M%S%f")[:-3] 8 | 9 | class DDEX: 10 | 11 | def __init__(self, sender, recipient, releases=[], resources=[], update=False): 12 | self.update = update 13 | self.releases = releases 14 | self.resources = resources 15 | self.sender = sender 16 | self.recipient = recipient 17 | 18 | def write(self, file_name): 19 | root = ET.Element("ernm:NewReleaseMessage", {'MessageSchemaVersionId': 'ern/341', 'LanguageAndScriptCode': 'en', 'xs:schemaLocation': 'http://ddex.net/xml/ern/341 http://ddex.net/xml/ern/341/release-notification.xsd', 'xmlns:ernm': 'http://ddex.net/xml/ern/341', 'xmlns:xs':'http://www.w3.org/2001/XMLSchema-instance'}) 20 | header = self.__write_message_header(root) 21 | root.append(header) 22 | 23 | update_indicator = ET.SubElement(root, "UpdateIndicator") 24 | if(self.update): 25 | update_indicator.text = "UpdateMessage" 26 | else: 27 | update_indicator.text = "OriginalMessage" 28 | 29 | resource_list = ET.SubElement(root, "ResourceList") 30 | for resource in self.resources: 31 | resource_list.append(resource.write()) 32 | 33 | release_list = ET.SubElement(root, "ReleaseList") 34 | deal_list = ET.SubElement(root, "DealList") 35 | 36 | for release in self.releases: 37 | release_list.append(release.write()) 38 | deal_list.append(release.write_deals()) 39 | 40 | tree = ET.ElementTree(root) 41 | tree.write(file_name) 42 | 43 | def __write_message_header(self, root): 44 | return MessageHeader(self.sender, self.recipient).write(); 45 | -------------------------------------------------------------------------------- /ddex/ddex_builder.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.ddex import DDEX 2 | from DDEXUI.ddex.resource import Resource 3 | from DDEXUI.ddex.release import ReleaseIdType 4 | 5 | class DDEXBuilder: 6 | def __init__(self): 7 | self._resources = [] 8 | self._releases = [] 9 | self._sender = None 10 | self._recipient = None 11 | self._is_update = None 12 | 13 | def recipient(self, recipient): 14 | self._recipient = recipient 15 | return self 16 | 17 | def sender(self, sender) : 18 | self._sender = sender 19 | return self 20 | 21 | def add_release(self, release): 22 | self._releases.append(release) 23 | return self 24 | 25 | def add_product_release(self, release): 26 | self._releases.insert(0, release) 27 | return self 28 | 29 | def add_resource(self, resource): 30 | self._resources.append(resource) 31 | return self 32 | 33 | def update(self, is_update): 34 | self._is_update = is_update 35 | return self 36 | 37 | def build(self): 38 | return DDEX(self._sender, self._recipient, self._releases, self._resources, self._is_update) 39 | 40 | def get_upc(self): 41 | try: 42 | return list(filter(lambda release: release.release_id.type == ReleaseIdType.Upc, self._releases))[0].release_id.id 43 | except Exception as e: 44 | print("No product release!") 45 | raise e 46 | -------------------------------------------------------------------------------- /ddex/deal.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as ET 2 | import datetime as date 3 | 4 | CommercialModals = ["PayAsYouGoModel", "SubscriptionModel"] 5 | UseTypes = ["PermanentDownload", "OnDemandStream"] 6 | Territories = ["UK", "FR", "DE", "US"] 7 | 8 | class Deal: 9 | 10 | def __init__(self, commercial_model, use_type, territory, start_date, preorder_date=None, preorder_preview_date=None): 11 | self.start_date = start_date 12 | self.preorder_date = preorder_date 13 | self.preorder_preview_date = preorder_preview_date 14 | self.territory = territory 15 | self.commercial_model = commercial_model 16 | self.use_type = use_type 17 | 18 | def write(self): 19 | deal = ET.Element("Deal") 20 | terms = self.__append_element_with_text(deal, "DealTerms") 21 | self.__append_element_with_text(terms, "CommercialModelType", self.commercial_model) 22 | usage = self.__append_element_with_text(terms, "Usage") 23 | self.__append_element_with_text(usage, "UseType", self.use_type) 24 | self.__append_element_with_text(terms, "TerritoryCode", self.territory) 25 | if(self.preorder_date != None): 26 | self.__append_element_with_text(terms, "PreorderReleaseDate", self.preorder_date.isoformat()) 27 | if(self.preorder_preview_date != None): 28 | self.__append_element_with_text(terms, "PreorderPreviewDate", self.preorder_preview_date.isoformat()) 29 | validity_period = self.__append_element_with_text(terms, "ValidityPeriod") 30 | self.__append_element_with_text(validity_period, "StartDate", self.start_date.isoformat()) 31 | return deal 32 | 33 | def __append_element_with_text(self, parent, name, text=""): 34 | el = ET.SubElement(parent, name) 35 | el.text = text 36 | return el 37 | -------------------------------------------------------------------------------- /ddex/enum.py: -------------------------------------------------------------------------------- 1 | def enum(**enums): 2 | reverse = dict((value, key) for key, value in enums.items()) 3 | enums['reverse_mapping'] = reverse 4 | return type('Enum', (), enums) 5 | 6 | -------------------------------------------------------------------------------- /ddex/file_metadata.py: -------------------------------------------------------------------------------- 1 | class FileMetadata: 2 | def __init__(self, md5, name, extension): 3 | self.md5 = md5 4 | self.name = name 5 | self.extension = extension 6 | 7 | class ImageFileMetadata: 8 | def __init__(self, md5, name, extension, width, height): 9 | FileMetadata.__init__(self, md5, name, extension) 10 | self.width = width 11 | self.height = height 12 | self.codec = "JPEG" 13 | 14 | class AudioFileMetadata(FileMetadata): 15 | def __init__(self, duration, bit_rate, md5, name, extension): 16 | FileMetadata.__init__(self, md5, name, extension) 17 | self.duration = duration 18 | self.bit_rate = bit_rate 19 | self.codec = "MP3" 20 | -------------------------------------------------------------------------------- /ddex/message_header.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as ET 2 | import datetime as d 3 | from uuid import uuid4 as uuid 4 | 5 | class MessageHeader: 6 | def __init__(self, sender, recipient): 7 | self.sender = sender 8 | self.recipient = recipient 9 | 10 | 11 | def write(self): 12 | message_header = ET.Element('MessageHeader') 13 | self.__add_element_with_text(message_header, "MessageThreadId", str(uuid())) 14 | self.__add_element_with_text(message_header, "MessageId", str(uuid())) 15 | message_header.append(self.sender.write()) 16 | message_header.append(self.recipient.write()) 17 | created_date = ET.SubElement(message_header, "MessageCreatedDateTime") 18 | created_date.text = d.datetime.now().replace(microsecond=0).isoformat()+ "Z" 19 | return message_header 20 | 21 | def __add_element_with_text(self, parent, name, text=""): 22 | element = ET.SubElement(parent, name) 23 | element.text = text 24 | -------------------------------------------------------------------------------- /ddex/party.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as ET 2 | from DDEXUI.ddex.enum import enum 3 | 4 | PartyType = enum(MessageSender=1, MessageRecipient=2) 5 | 6 | class Party: 7 | def __init__(self, party_id, name, party_type=PartyType.MessageSender): 8 | self.party_id = party_id 9 | self.name = name 10 | self.party_type = party_type 11 | 12 | def write(self): 13 | party = ET.Element(PartyType.reverse_mapping[self.party_type]) 14 | party_id = ET.SubElement(party,'PartyId') 15 | party_id.text = self.party_id 16 | name = ET.SubElement(party, 'PartyName') 17 | full_name = ET.SubElement(name, 'FullName') 18 | full_name .text = self.name 19 | return party 20 | 21 | def __eq__(self, other): 22 | if(isinstance(other, Party)): 23 | return self.name == other.name and self.party_id == other.party_id and self.party_type == other.party_type 24 | return NotImplemented 25 | 26 | def __str__(self): 27 | return str.join(":",[self.party_id,self.name,PartyType.reverse_mapping[self.party_type]]) 28 | 29 | def __ne__(self, other): 30 | result = self.__eq__(other) 31 | if(result is NotImplemented): 32 | return result 33 | return not result 34 | 35 | 36 | -------------------------------------------------------------------------------- /ddex/release.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as ET 2 | from DDEXUI.ddex.enum import enum 3 | 4 | ReleaseIdType = enum(Upc=1, Isrc=2) 5 | ReleaseType = enum(Single=1) 6 | 7 | class ReleaseId: 8 | def __init__(self, type, id): 9 | self.type = type 10 | self.id = id 11 | 12 | def write(self): 13 | name = None 14 | attrs = {} 15 | if(self.type == ReleaseIdType.Upc): 16 | name = "ICPN" 17 | attrs = {"IsEan": "false"} 18 | elif(self.type == ReleaseIdType.Isrc): 19 | name = "ISRC" 20 | element = ET.Element(name, attrs) 21 | element.text = self.id 22 | return element 23 | 24 | class Release: 25 | def __init__(self, title, cline, pline, year, release_reference, release_id, release_type, artist, label, parental_warning): 26 | self.release_type = release_type 27 | self.release_id = release_id 28 | self.genres = [] 29 | self.pline = pline 30 | self.release_reference = release_reference 31 | self.cline = cline 32 | self.year = str(year) 33 | self.title = title 34 | self.artist = artist 35 | self.label = label 36 | self.deals = [] 37 | self.release_resource_references = [] 38 | if(parental_warning): 39 | self.parental_warning = "Explicit" 40 | else: 41 | self.parental_warning = "NotExplicit" 42 | 43 | def write(self): 44 | release = ET.Element("Release") 45 | releaseId = ET.SubElement(release, "ReleaseId") 46 | releaseId.append(self.release_id.write()) 47 | self.__add_element(release, "ReleaseReference", self.release_reference) 48 | referenceTitle = ET.SubElement(release, "ReferenceTitle") 49 | self.__add_element(referenceTitle, "TitleText", self.title) 50 | resource_reference_list = ET.SubElement(release, "ReleaseResourceReferenceList") 51 | 52 | resource_group = ET.Element("ResourceGroup") 53 | i = 0 54 | for ref, ref_type in self.release_resource_references: 55 | self.__add_element(resource_reference_list, "ReleaseResourceReference", ref, {"ReleaseResourceType":ref_type}) 56 | content_item = self.__add_element(resource_group, "ResourceGroupContentItem") 57 | self.__add_element(content_item, "SequenceNumber", str(i + 1)) 58 | self.__add_element(content_item, "ResourceType", "SoundRecording") 59 | self.__add_element(content_item, "ReleaseResourceReference", ref) 60 | i = i+1 61 | 62 | self.__add_element(release, "ReleaseType", self.release_type) 63 | release_details_by_territory = ET.SubElement(release, "ReleaseDetailsByTerritory") 64 | ET.SubElement(release_details_by_territory, "TerritoryCode").text = "Worldwide" 65 | self.__add_element(release_details_by_territory, "DisplayArtistName", self.artist) 66 | self.__add_element(release_details_by_territory, "LabelName", self.label) 67 | self.__write_titles(release, release_details_by_territory) 68 | self.__write_genres(release_details_by_territory) 69 | self.__write_artist(release_details_by_territory) 70 | self.__add_element(release_details_by_territory, "ParentalWarningType", self.parental_warning) 71 | release_details_by_territory.append(resource_group) 72 | pline = ET.SubElement(release, "PLine") 73 | self.__add_element(pline, "Year", self.year) 74 | self.__add_element(pline, "PLineText", self.pline) 75 | cline = ET.SubElement(release, "CLine") 76 | self.__add_element(cline, "Year", self.year) 77 | self.__add_element(cline, "CLineText", self.cline) 78 | return release 79 | 80 | def __add_element(self, parent, name, text="", attrs={}): 81 | element = ET.SubElement(parent, name, attrs) 82 | element.text = text 83 | return element 84 | 85 | def __write_artist(self, release_details_by_territory): 86 | artist = ET.SubElement(release_details_by_territory, "DisplayArtist") 87 | party_name = ET.SubElement(artist, "PartyName") 88 | self.__add_element(party_name, "FullName", self.artist) 89 | self.__add_element(artist, "ArtistRole", "MainArtist") 90 | 91 | def __write_genres(self, release_details_by_territory): 92 | for genre in self.genres: 93 | genreElement = ET.SubElement(release_details_by_territory, "Genre") 94 | ET.SubElement(genreElement, "GenreText").text = genre 95 | 96 | def __write_titles(self, release, release_details_by_territory): 97 | for type in ["FormalTitle", "DisplayTitle", "GroupingTitle"]: 98 | self.__add_title(release_details_by_territory, type) 99 | 100 | def __add_title(self, release_details_by_territory, type): 101 | title = ET.SubElement(release_details_by_territory, "Title", {"TitleType": type}) 102 | self.__add_element(title, "TitleText", self.title) 103 | 104 | def add_deal(self, deal): 105 | self.deals.append(deal) 106 | 107 | def add_resource_reference(self, reference, release_resource_type="PrimaryResource"): 108 | self.release_resource_references.append((reference, release_resource_type)) 109 | 110 | def write_deals(self): 111 | release_deal = ET.Element("ReleaseDeal") 112 | self.__add_element(release_deal, "DealReleaseReference", self.release_reference) 113 | for deal in self.deals: 114 | release_deal.append(deal.write()) 115 | return release_deal 116 | -------------------------------------------------------------------------------- /ddex/release_builder.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.release import * 2 | 3 | class ReleaseBuilder: 4 | def __init__(self): 5 | self._deals = [] 6 | self._resources = set() 7 | self._title = None 8 | self._cline = None 9 | self._pline = None 10 | self._year = None 11 | self._reference = None 12 | self._release_id = None 13 | self._release_type = None 14 | self._artist = None 15 | self._label = None 16 | self._warning = None 17 | 18 | def title(self, title): 19 | self._title = title 20 | return self 21 | 22 | def c_line(self, cline): 23 | self._cline = cline 24 | return self 25 | 26 | def p_line(self, pline): 27 | self._pline = pline 28 | return self 29 | 30 | def year(self, year): 31 | self._year = year 32 | return self 33 | 34 | def reference(self, reference): 35 | if(type(reference) != str): 36 | raise TypeError("resource reference must be a str") 37 | self._reference = reference 38 | return self 39 | 40 | def release_id(self, id_type, id_value): 41 | self._release_id = ReleaseId(id_type, id_value) 42 | return self 43 | 44 | def release_type(self, release_type): 45 | self._release_type = release_type 46 | return self 47 | 48 | def artist(self, artist): 49 | self._artist = artist 50 | return self 51 | 52 | def label(self, label): 53 | self._label = label 54 | return self 55 | 56 | def parental_warning(self, warning): 57 | self._warning = warning 58 | return self 59 | 60 | def add_deal(self, deal): 61 | self._deals.append(deal) 62 | return self 63 | 64 | def add_resource(self, resource_reference): 65 | self._resources.add(resource_reference) 66 | return self 67 | 68 | def build(self): 69 | release = (Release(self._title, 70 | self._cline, 71 | self._pline, 72 | self._year, 73 | self._reference, 74 | self._release_id, 75 | self._release_type, 76 | self._artist, 77 | self._label, 78 | self._warning)) 79 | 80 | for reference in self._resources: 81 | release.add_resource_reference(reference) 82 | for deal in self._deals: 83 | release.add_deal(deal) 84 | return release 85 | 86 | def get_isrc(self): 87 | if(self._release_id.type == ReleaseIdType.Isrc): 88 | return self._release_id.id 89 | 90 | def get_title(self): 91 | return self._title 92 | -------------------------------------------------------------------------------- /ddex/resource.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as ET 2 | from abc import ABCMeta, abstractmethod 3 | 4 | class Resource(metaclass=ABCMeta): 5 | def __init__(self, technical_resource_details_reference, id_attrs={}): 6 | self._id_attrs = id_attrs 7 | self._technical_resource_details_reference = technical_resource_details_reference 8 | 9 | @abstractmethod 10 | def write(self): 11 | resource = ET.Element(self.kind()) 12 | self._append_element_with_text(resource, self.kind()+"Type", self.type()) 13 | resource_id = self._append_element_with_text(resource, self.kind()+"Id") 14 | self._append_element_with_text(resource_id, self.id_type(), self.id_value(), self._id_attrs) 15 | self._append_element_with_text(resource, "ResourceReference", self.resource_reference()) 16 | return resource 17 | 18 | @abstractmethod 19 | def _append_technical_details(self, resource, technical_resource_details_reference): 20 | details_by_territory = self._append_element_with_text(resource, self.kind()+"DetailsByTerritory") 21 | self._append_element_with_text(details_by_territory, "TerritoryCode", "Worldwide") 22 | technical_details = ET.SubElement(details_by_territory, "Technical"+self.kind()+"Details") 23 | self._append_element_with_text(technical_details, "TechnicalResourceDetailsReference", technical_resource_details_reference) 24 | return technical_details 25 | 26 | def _append_file(self, technical_details, file_metadata): 27 | file_element = ET.SubElement(technical_details, "File") 28 | self._append_element_with_text(file_element, "FileName", file_metadata.name) 29 | hash_sum = ET.SubElement(file_element, "HashSum") 30 | self._append_element_with_text(hash_sum, "HashSum", file_metadata.md5) 31 | self._append_element_with_text(hash_sum, "HashSumAlgorithmType", "MD5") 32 | 33 | @property 34 | @abstractmethod 35 | def kind(self): 36 | pass 37 | 38 | @property 39 | @abstractmethod 40 | def type(self): 41 | pass 42 | 43 | @property 44 | @abstractmethod 45 | def id_type(self): 46 | pass 47 | 48 | @property 49 | @abstractmethod 50 | def id_value(self): 51 | pass 52 | 53 | @property 54 | @abstractmethod 55 | def resource_reference(self): 56 | pass 57 | 58 | @property 59 | def technical_resource_details_reference(self): 60 | return self._technical_resource_details_reference 61 | 62 | def _append_element_with_text(self, parent, name, text="", attrs={}): 63 | el = ET.SubElement(parent, name, attrs) 64 | el.text = text 65 | return el 66 | 67 | 68 | class Image(Resource): 69 | def __init__(self, resource_reference, id_value, file_metadata, technical_resource_details_reference): 70 | Resource.__init__(self, technical_resource_details_reference, {"Namespace": "DDEXUI"}) 71 | self.__resource_reference = resource_reference 72 | self.__id_value = id_value 73 | self.file_metadata = file_metadata 74 | 75 | def write(self): 76 | resource = super().write() 77 | self._append_technical_details(resource) 78 | return resource 79 | 80 | def _append_technical_details(self, resource): 81 | technical_details = super()._append_technical_details(resource, self._technical_resource_details_reference) 82 | self._append_element_with_text(technical_details, "ImageCodecType", self.file_metadata.codec) 83 | self._append_element_with_text(technical_details, "ImageHeight", str(self.file_metadata.height)) 84 | self._append_element_with_text(technical_details, "ImageWidth", str(self.file_metadata.width)) 85 | self._append_file(technical_details, self.file_metadata) 86 | 87 | def kind(self): 88 | return "Image" 89 | 90 | def type(self): 91 | return "FrontCoverImage" 92 | 93 | def id_value(self): 94 | return self.__id_value 95 | 96 | def id_type(self): 97 | return "ProprietaryId" 98 | 99 | def resource_reference(self): 100 | return self.__resource_reference 101 | 102 | class SoundRecording(Resource): 103 | def __init__(self, resource_reference, isrc, title, file_metadata, technical_resource_details_reference): 104 | Resource.__init__(self, technical_resource_details_reference) 105 | self.title = title 106 | self.__resource_reference = resource_reference 107 | self.isrc = isrc 108 | self.file_metadata = file_metadata 109 | 110 | def write(self): 111 | sound_recording = super().write() 112 | title = self._append_element_with_text(sound_recording, "ReferenceTitle") 113 | self._append_element_with_text(title, "TitleText", self.title) 114 | 115 | self._append_element_with_text(sound_recording, "Duration", self.file_metadata.duration) 116 | 117 | self._append_technical_details(sound_recording) 118 | return sound_recording 119 | 120 | def _append_technical_details(self, resource): 121 | technical_details = super()._append_technical_details(resource, self._technical_resource_details_reference) 122 | self._append_element_with_text(technical_details, "AudioCodecType", self.file_metadata.codec) 123 | self._append_file(technical_details, self.file_metadata) 124 | 125 | def kind(self): 126 | return "SoundRecording" 127 | 128 | def type(self): 129 | return "MusicalWorkSoundRecording" 130 | 131 | def id_value(self): 132 | return self.isrc 133 | 134 | def id_type(self): 135 | return "ISRC" 136 | 137 | def resource_reference(self): 138 | return self.__resource_reference 139 | -------------------------------------------------------------------------------- /ddex/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willm/DDEXUI/c10e879cf1a32bcd26005f215f0693d3015b9f29/ddex/tests/__init__.py -------------------------------------------------------------------------------- /ddex/tests/data.py: -------------------------------------------------------------------------------- 1 | import random 2 | from DDEXUI.ddex.ddex_builder import DDEXBuilder 3 | from DDEXUI.ddex.release_builder import ReleaseBuilder 4 | from DDEXUI.ddex.party import Party, PartyType 5 | from DDEXUI.ddex.release import * 6 | from DDEXUI.ddex.deal import * 7 | from datetime import datetime 8 | 9 | 10 | def valid_ddex_builder(): 11 | upc = str(random.randrange(100000000000, 9999999999999)) 12 | return (DDEXBuilder().sender(Party("XD234241EW1", "Hospital Records", PartyType.MessageSender)) 13 | .update(False) 14 | .recipient(Party("RDG2342424ES", "Bobs Records", PartyType.MessageSender)) 15 | .add_release(valid_product_release(upc))) 16 | 17 | 18 | def valid_product_release(upc): 19 | return (ReleaseBuilder().title("Racing Green") 20 | .c_line("Copyright hospital records") 21 | .p_line("Published by Westbury Music") 22 | .year(2004) 23 | .reference("A0") 24 | .release_id(ReleaseIdType.Upc, upc) 25 | .release_type("Single")#ReleaseType.Single) 26 | .artist("High Contrast") 27 | .label("Hospital Records") 28 | .parental_warning(False) 29 | .add_deal(Deal("PayAsYouGoModel", "PermanentDownload", "FR", datetime(2004, 9, 6))) 30 | .build()) 31 | 32 | def valid_track_release(isrc): 33 | return (ReleaseBuilder().title("Racing Green") 34 | .c_line("Copyright hospital records") 35 | .p_line("Published by Westbury Music") 36 | .year(2004) 37 | .reference("A0") 38 | .release_id(ReleaseIdType.Isrc, isrc) 39 | .release_type("Track") 40 | .artist("High Contrast") 41 | .label("Hospital Records") 42 | .parental_warning(False) 43 | .add_deal(Deal("PayAsYouGoModel", "PermanentDownload", "FR", datetime(2004, 9, 6))) 44 | .build()) 45 | -------------------------------------------------------------------------------- /ddex/tests/data_helper.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.release_builder import * 2 | from DDEXUI.ddex.release import * 3 | 4 | class TestData: 5 | @staticmethod 6 | def release_builder(): 7 | return (ReleaseBuilder().title("Black Sands") 8 | .c_line("copyright ninja tune") 9 | .p_line("published by ninja") 10 | .year(2010) 11 | .reference("R0") 12 | .release_id(ReleaseIdType.Upc, "5021392584126") 13 | .release_type(ReleaseType.Single) 14 | .artist("Bonobo") 15 | .label("Ninja Tune") 16 | .parental_warning(True)) 17 | 18 | -------------------------------------------------------------------------------- /ddex/tests/resources/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willm/DDEXUI/c10e879cf1a32bcd26005f215f0693d3015b9f29/ddex/tests/resources/test.jpg -------------------------------------------------------------------------------- /ddex/tests/resources/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willm/DDEXUI/c10e879cf1a32bcd26005f215f0693d3015b9f29/ddex/tests/resources/test.mp3 -------------------------------------------------------------------------------- /ddex/tests/resources/xsds/iso4217a.xsd: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | © 2006-2012 Digital Data Exchange, LLC (DDEX) 9 | 10 | 11 | 12 | An ISO4217 three-letter code representing a ddex:Currency. 13 | 14 | 15 | 16 | 17 | UAE Dirham. 18 | 19 | 20 | 21 | 22 | Afghani. 23 | 24 | 25 | 26 | 27 | Lek. 28 | 29 | 30 | 31 | 32 | Armenian Dram. 33 | 34 | 35 | 36 | 37 | Netherlands Antillian Guilder. 38 | 39 | 40 | 41 | 42 | Kwanza. 43 | 44 | 45 | 46 | 47 | Argentine Peso. 48 | 49 | 50 | 51 | 52 | Australian Dollar. 53 | 54 | 55 | 56 | 57 | Aruban Guilder. 58 | 59 | 60 | 61 | 62 | Azerbaijanian Manat. 63 | 64 | 65 | 66 | 67 | Convertible Marks. 68 | 69 | 70 | 71 | 72 | Barbados Dollar. 73 | 74 | 75 | 76 | 77 | Taka. 78 | 79 | 80 | 81 | 82 | Bulgarian Lev. 83 | 84 | 85 | 86 | 87 | Bahraini Dinar. 88 | 89 | 90 | 91 | 92 | Burundi Franc. 93 | 94 | 95 | 96 | 97 | Bermudian Dollar. 98 | 99 | 100 | 101 | 102 | Brunei Dollar. 103 | 104 | 105 | 106 | 107 | Boliviano. 108 | 109 | 110 | 111 | 112 | Mvdol. 113 | 114 | 115 | 116 | 117 | Brazilian Real. 118 | 119 | 120 | 121 | 122 | Bahamian Dollar. 123 | 124 | 125 | 126 | 127 | Ngultrum. 128 | 129 | 130 | 131 | 132 | Pula. 133 | 134 | 135 | 136 | 137 | Belarussian Ruble. 138 | 139 | 140 | 141 | 142 | Belize Dollar. 143 | 144 | 145 | 146 | 147 | Canadian Dollar. 148 | 149 | 150 | 151 | 152 | Congolese Franc. 153 | 154 | 155 | 156 | 157 | Swiss Franc. 158 | 159 | 160 | 161 | 162 | Unidades de fomento. 163 | 164 | 165 | 166 | 167 | Chilean Peso. 168 | 169 | 170 | 171 | 172 | Yuan Renminbi. 173 | 174 | 175 | 176 | 177 | Colombian Peso. 178 | 179 | 180 | 181 | 182 | Unidad de Valor Real. 183 | 184 | 185 | 186 | 187 | Costa Rican Colón. 188 | 189 | 190 | 191 | 192 | Peso Convertible. 193 | 194 | 195 | 196 | 197 | Cuban Peso. 198 | 199 | 200 | 201 | 202 | Cape Verde Escudo. 203 | 204 | 205 | 206 | 207 | Czech Koruna. 208 | 209 | 210 | 211 | 212 | Djibouti Franc. 213 | 214 | 215 | 216 | 217 | Danish Krone. 218 | 219 | 220 | 221 | 222 | Dominican Peso. 223 | 224 | 225 | 226 | 227 | Algerian Dinar. 228 | 229 | 230 | 231 | 232 | Kroon. 233 | 234 | 235 | 236 | 237 | Egyptian Pound. 238 | 239 | 240 | 241 | 242 | Nakfa. 243 | 244 | 245 | 246 | 247 | Ethiopian Birr. 248 | 249 | 250 | 251 | 252 | Euro. 253 | 254 | 255 | 256 | 257 | Fiji Dollar. 258 | 259 | 260 | 261 | 262 | Falkland Islands Pound. 263 | 264 | 265 | 266 | 267 | Pound Sterling. 268 | 269 | 270 | 271 | 272 | Lari. 273 | 274 | 275 | 276 | 277 | Cedi. 278 | 279 | 280 | 281 | 282 | Gibraltar Pound. 283 | 284 | 285 | 286 | 287 | Dalasi. 288 | 289 | 290 | 291 | 292 | Guinea Franc. 293 | 294 | 295 | 296 | 297 | Quetzal. 298 | 299 | 300 | 301 | 302 | Guyana Dollar. 303 | 304 | 305 | 306 | 307 | Hong Kong Dollar. 308 | 309 | 310 | 311 | 312 | Lempira. 313 | 314 | 315 | 316 | 317 | Croatian Kuna. 318 | 319 | 320 | 321 | 322 | Gourde. 323 | 324 | 325 | 326 | 327 | Forint. 328 | 329 | 330 | 331 | 332 | Rupiah. 333 | 334 | 335 | 336 | 337 | New Israeli Sheqel. 338 | 339 | 340 | 341 | 342 | Indian Rupee. 343 | 344 | 345 | 346 | 347 | Iraqi Dinar. 348 | 349 | 350 | 351 | 352 | Iranian Rial. 353 | 354 | 355 | 356 | 357 | Iceland Króna. 358 | 359 | 360 | 361 | 362 | Jamaican Dollar. 363 | 364 | 365 | 366 | 367 | Jordanian Dinar. 368 | 369 | 370 | 371 | 372 | Yen. 373 | 374 | 375 | 376 | 377 | Kenyan Shilling. 378 | 379 | 380 | 381 | 382 | Som. 383 | 384 | 385 | 386 | 387 | Riel. 388 | 389 | 390 | 391 | 392 | Comoro Franc. 393 | 394 | 395 | 396 | 397 | North Korean Won. 398 | 399 | 400 | 401 | 402 | Won. 403 | 404 | 405 | 406 | 407 | Kuwaiti Dinar. 408 | 409 | 410 | 411 | 412 | Cayman Islands Dollar. 413 | 414 | 415 | 416 | 417 | Tenge. 418 | 419 | 420 | 421 | 422 | Kip. 423 | 424 | 425 | 426 | 427 | Lebanese Pound. 428 | 429 | 430 | 431 | 432 | Sri Lanka Rupee. 433 | 434 | 435 | 436 | 437 | Liberian Dollar. 438 | 439 | 440 | 441 | 442 | Loti. 443 | 444 | 445 | 446 | 447 | Lithuanian Litas. 448 | 449 | 450 | 451 | 452 | Latvian Lats. 453 | 454 | 455 | 456 | 457 | Libyan Dinar. 458 | 459 | 460 | 461 | 462 | Moroccan Dirham. 463 | 464 | 465 | 466 | 467 | Moldovan Leu. 468 | 469 | 470 | 471 | 472 | Malagasy Ariary. 473 | 474 | 475 | 476 | 477 | Denar. 478 | 479 | 480 | 481 | 482 | Kyat. 483 | 484 | 485 | 486 | 487 | Tugrik. 488 | 489 | 490 | 491 | 492 | Pataca. 493 | 494 | 495 | 496 | 497 | Ouguiya. 498 | 499 | 500 | 501 | 502 | Mauritius Rupee. 503 | 504 | 505 | 506 | 507 | Rufiyaa. 508 | 509 | 510 | 511 | 512 | Kwacha. 513 | 514 | 515 | 516 | 517 | Mexican Peso. 518 | 519 | 520 | 521 | 522 | Mexican Unidad de Inversion. 523 | 524 | 525 | 526 | 527 | Malaysian Ringgit. 528 | 529 | 530 | 531 | 532 | Metical. 533 | 534 | 535 | 536 | 537 | Namibia Dollar. 538 | 539 | 540 | 541 | 542 | Naira. 543 | 544 | 545 | 546 | 547 | Córdoba Oro. 548 | 549 | 550 | 551 | 552 | Norwegian Krone. 553 | 554 | 555 | 556 | 557 | Nepalese Rupee. 558 | 559 | 560 | 561 | 562 | New Zealand Dollar. 563 | 564 | 565 | 566 | 567 | Rial Omani. 568 | 569 | 570 | 571 | 572 | Balboa. 573 | 574 | 575 | 576 | 577 | Nuevo Sol. 578 | 579 | 580 | 581 | 582 | Kina. 583 | 584 | 585 | 586 | 587 | Philippine Peso. 588 | 589 | 590 | 591 | 592 | Pakistan Rupee. 593 | 594 | 595 | 596 | 597 | Zloty. 598 | 599 | 600 | 601 | 602 | Guarani. 603 | 604 | 605 | 606 | 607 | Qatari Rial. 608 | 609 | 610 | 611 | 612 | New Leu. 613 | 614 | 615 | 616 | 617 | Serbian Dinar. 618 | 619 | 620 | 621 | 622 | Russian Ruble. 623 | 624 | 625 | 626 | 627 | Rwanda Franc. 628 | 629 | 630 | 631 | 632 | Saudi Riyal. 633 | 634 | 635 | 636 | 637 | Solomon Islands Dollar. 638 | 639 | 640 | 641 | 642 | Seychelles Rupee. 643 | 644 | 645 | 646 | 647 | Sudanese Pound. 648 | 649 | 650 | 651 | 652 | Swedish Krona. 653 | 654 | 655 | 656 | 657 | Singapore Dollar. 658 | 659 | 660 | 661 | 662 | Saint Helena Pound. 663 | 664 | 665 | 666 | 667 | Leone. 668 | 669 | 670 | 671 | 672 | Somali Shilling. 673 | 674 | 675 | 676 | 677 | Suriname Dollar. 678 | 679 | 680 | 681 | 682 | Dobra. 683 | 684 | 685 | 686 | 687 | El Salvador Colón. 688 | 689 | 690 | 691 | 692 | Syrian Pound. 693 | 694 | 695 | 696 | 697 | Lilangeni. 698 | 699 | 700 | 701 | 702 | Baht. 703 | 704 | 705 | 706 | 707 | Somoni. 708 | 709 | 710 | 711 | 712 | Manat. 713 | 714 | 715 | 716 | 717 | Tunisian Dinar. 718 | 719 | 720 | 721 | 722 | Pa'anga. 723 | 724 | 725 | 726 | 727 | Turkish Lira. 728 | 729 | 730 | 731 | 732 | Trinidad and Tobago Dollar. 733 | 734 | 735 | 736 | 737 | New Taiwan Dollar. 738 | 739 | 740 | 741 | 742 | Tanzanian Shilling. 743 | 744 | 745 | 746 | 747 | Hryvnia. 748 | 749 | 750 | 751 | 752 | Uganda Shilling. 753 | 754 | 755 | 756 | 757 | US Dollar. 758 | 759 | 760 | 761 | 762 | Uruguay Peso en Unidades Indexadas. 763 | 764 | 765 | 766 | 767 | Peso Uruguayo. 768 | 769 | 770 | 771 | 772 | Uzbekistan Sum. 773 | 774 | 775 | 776 | 777 | Bolivar Fuerte. 778 | 779 | 780 | 781 | 782 | Dong. 783 | 784 | 785 | 786 | 787 | Vatu. 788 | 789 | 790 | 791 | 792 | Tala. 793 | 794 | 795 | 796 | 797 | CFA Franc BEAC. 798 | 799 | 800 | 801 | 802 | East Caribbean Dollar. 803 | 804 | 805 | 806 | 807 | CFA Franc BCEAO. 808 | 809 | 810 | 811 | 812 | CFP Franc. 813 | 814 | 815 | 816 | 817 | Yemeni Rial. 818 | 819 | 820 | 821 | 822 | Rand. 823 | 824 | 825 | 826 | 827 | Zambian Kwacha. 828 | 829 | 830 | 831 | 832 | Zimbabwe Dollar. 833 | 834 | 835 | 836 | 837 | -------------------------------------------------------------------------------- /ddex/tests/resources/xsds/iso639a2.xsd: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | © 2006-2012 Digital Data Exchange, LLC (DDEX) 9 | 10 | 11 | 12 | An ISO639-1 two-letter code representing a ddex:Language. 13 | 14 | 15 | 16 | 17 | Afar. 18 | 19 | 20 | 21 | 22 | Abkhazian. 23 | 24 | 25 | 26 | 27 | Afrikaans. 28 | 29 | 30 | 31 | 32 | Akan. 33 | 34 | 35 | 36 | 37 | Albanian. 38 | 39 | 40 | 41 | 42 | Amharic. 43 | 44 | 45 | 46 | 47 | Arabic. 48 | 49 | 50 | 51 | 52 | Aragonese. 53 | 54 | 55 | 56 | 57 | Armenian. 58 | 59 | 60 | 61 | 62 | Assamese. 63 | 64 | 65 | 66 | 67 | Avaric. 68 | 69 | 70 | 71 | 72 | Avestan. 73 | 74 | 75 | 76 | 77 | Aymara. 78 | 79 | 80 | 81 | 82 | Azerbaijani. 83 | 84 | 85 | 86 | 87 | Bashkir. 88 | 89 | 90 | 91 | 92 | Bambara. 93 | 94 | 95 | 96 | 97 | Basque. 98 | 99 | 100 | 101 | 102 | Belarusian. 103 | 104 | 105 | 106 | 107 | Bengali. 108 | 109 | 110 | 111 | 112 | Bihari. 113 | 114 | 115 | 116 | 117 | Bislama. 118 | 119 | 120 | 121 | 122 | Tibetan. 123 | 124 | 125 | 126 | 127 | Bosnian. 128 | 129 | 130 | 131 | 132 | Breton. 133 | 134 | 135 | 136 | 137 | Bulgarian. 138 | 139 | 140 | 141 | 142 | Burmese. 143 | 144 | 145 | 146 | 147 | Catalan or Valencian. 148 | 149 | 150 | 151 | 152 | Czech. 153 | 154 | 155 | 156 | 157 | Chamorro. 158 | 159 | 160 | 161 | 162 | Chechen. 163 | 164 | 165 | 166 | 167 | Chinese. 168 | 169 | 170 | 171 | 172 | Church Slavic or Old Slavonic or Church Slavonic or Old Bulgarian or Old Church Slavonic. 173 | 174 | 175 | 176 | 177 | Chuvash. 178 | 179 | 180 | 181 | 182 | Cornish. 183 | 184 | 185 | 186 | 187 | Corsican. 188 | 189 | 190 | 191 | 192 | Cree. 193 | 194 | 195 | 196 | 197 | Welsh. 198 | 199 | 200 | 201 | 202 | Danish. 203 | 204 | 205 | 206 | 207 | German. 208 | 209 | 210 | 211 | 212 | Divehi. 213 | 214 | 215 | 216 | 217 | Dutch or Flemish. 218 | 219 | 220 | 221 | 222 | Dzongkha. 223 | 224 | 225 | 226 | 227 | Modern Greek (1453-). 228 | 229 | 230 | 231 | 232 | English. 233 | 234 | 235 | 236 | 237 | Esperanto. 238 | 239 | 240 | 241 | 242 | Estonian. 243 | 244 | 245 | 246 | 247 | Ewe. 248 | 249 | 250 | 251 | 252 | Faroese. 253 | 254 | 255 | 256 | 257 | Persian. 258 | 259 | 260 | 261 | 262 | Fijian. 263 | 264 | 265 | 266 | 267 | Finnish. 268 | 269 | 270 | 271 | 272 | French. 273 | 274 | 275 | 276 | 277 | Frisian. 278 | 279 | 280 | 281 | 282 | Fulah. 283 | 284 | 285 | 286 | 287 | Georgian. 288 | 289 | 290 | 291 | 292 | Gaelic or Scottish Gaelic. 293 | 294 | 295 | 296 | 297 | Irish. 298 | 299 | 300 | 301 | 302 | Gallegan. 303 | 304 | 305 | 306 | 307 | Manx. 308 | 309 | 310 | 311 | 312 | Guarani. 313 | 314 | 315 | 316 | 317 | Gujarati. 318 | 319 | 320 | 321 | 322 | Haitian or Haitian Creole. 323 | 324 | 325 | 326 | 327 | Hausa. 328 | 329 | 330 | 331 | 332 | Hebrew. 333 | 334 | 335 | 336 | 337 | Herero. 338 | 339 | 340 | 341 | 342 | Hindi. 343 | 344 | 345 | 346 | 347 | Hiri Motu. 348 | 349 | 350 | 351 | 352 | Croatian. 353 | 354 | 355 | 356 | 357 | Hungarian. 358 | 359 | 360 | 361 | 362 | Igbo. 363 | 364 | 365 | 366 | 367 | Icelandic. 368 | 369 | 370 | 371 | 372 | Ido. 373 | 374 | 375 | 376 | 377 | Sichuan Yi. 378 | 379 | 380 | 381 | 382 | Inuktitut. 383 | 384 | 385 | 386 | 387 | Interlingue. 388 | 389 | 390 | 391 | 392 | Interlingua (International Auxiliary Language Association). 393 | 394 | 395 | 396 | 397 | Indonesian. 398 | 399 | 400 | 401 | 402 | Inupiaq. 403 | 404 | 405 | 406 | 407 | Italian. 408 | 409 | 410 | 411 | 412 | Javanese. 413 | 414 | 415 | 416 | 417 | Japanese. 418 | 419 | 420 | 421 | 422 | Kalaallisut or Greenlandic. 423 | 424 | 425 | 426 | 427 | Kannada. 428 | 429 | 430 | 431 | 432 | Kashmiri. 433 | 434 | 435 | 436 | 437 | Kanuri. 438 | 439 | 440 | 441 | 442 | Kazakh. 443 | 444 | 445 | 446 | 447 | Khmer. 448 | 449 | 450 | 451 | 452 | Kikuyu or Gikuyu. 453 | 454 | 455 | 456 | 457 | Kinyarwanda. 458 | 459 | 460 | 461 | 462 | Kirghiz. 463 | 464 | 465 | 466 | 467 | Komi. 468 | 469 | 470 | 471 | 472 | Kongo. 473 | 474 | 475 | 476 | 477 | Korean. 478 | 479 | 480 | 481 | 482 | Kuanyama or Kwanyama. 483 | 484 | 485 | 486 | 487 | Kurdish. 488 | 489 | 490 | 491 | 492 | Lao. 493 | 494 | 495 | 496 | 497 | Latin. 498 | 499 | 500 | 501 | 502 | Latvian. 503 | 504 | 505 | 506 | 507 | Limburgan or Limburger or Limburgish. 508 | 509 | 510 | 511 | 512 | Lingala. 513 | 514 | 515 | 516 | 517 | Lithuanian. 518 | 519 | 520 | 521 | 522 | Luxembourgish or Letzeburgesch. 523 | 524 | 525 | 526 | 527 | Luba-Katanga. 528 | 529 | 530 | 531 | 532 | Ganda. 533 | 534 | 535 | 536 | 537 | Macedonian. 538 | 539 | 540 | 541 | 542 | Marshallese. 543 | 544 | 545 | 546 | 547 | Malayalam. 548 | 549 | 550 | 551 | 552 | Maori. 553 | 554 | 555 | 556 | 557 | Marathi. 558 | 559 | 560 | 561 | 562 | Malay. 563 | 564 | 565 | 566 | 567 | Malagasy. 568 | 569 | 570 | 571 | 572 | Maltese. 573 | 574 | 575 | 576 | 577 | Moldavian. 578 | 579 | 580 | 581 | 582 | Mongolian. 583 | 584 | 585 | 586 | 587 | Nauru. 588 | 589 | 590 | 591 | 592 | Navajo or Navaho. 593 | 594 | 595 | 596 | 597 | South Ndebele. 598 | 599 | 600 | 601 | 602 | North Ndebele. 603 | 604 | 605 | 606 | 607 | Ndonga. 608 | 609 | 610 | 611 | 612 | Nepali. 613 | 614 | 615 | 616 | 617 | Norwegian Nynorsk. 618 | 619 | 620 | 621 | 622 | Norwegian Bokmål 623 | 624 | 625 | 626 | 627 | Norwegian. 628 | 629 | 630 | 631 | 632 | Chichewa or Chewa or Nyanja. 633 | 634 | 635 | 636 | 637 | Occitan (post 1500) or Provençal. 638 | 639 | 640 | 641 | 642 | Ojibwa. 643 | 644 | 645 | 646 | 647 | Oriya. 648 | 649 | 650 | 651 | 652 | Oromo. 653 | 654 | 655 | 656 | 657 | Ossetian or Ossetic. 658 | 659 | 660 | 661 | 662 | Panjabi or Punjabi. 663 | 664 | 665 | 666 | 667 | Pali. 668 | 669 | 670 | 671 | 672 | Polish. 673 | 674 | 675 | 676 | 677 | Portuguese. 678 | 679 | 680 | 681 | 682 | Pushto. 683 | 684 | 685 | 686 | 687 | Quechua. 688 | 689 | 690 | 691 | 692 | Raeto-Romance. 693 | 694 | 695 | 696 | 697 | Romanian. 698 | 699 | 700 | 701 | 702 | Rundi. 703 | 704 | 705 | 706 | 707 | Russian. 708 | 709 | 710 | 711 | 712 | Sango. 713 | 714 | 715 | 716 | 717 | Sanskrit. 718 | 719 | 720 | 721 | 722 | Serbian. 723 | 724 | 725 | 726 | 727 | Sinhalese. 728 | 729 | 730 | 731 | 732 | Slovak. 733 | 734 | 735 | 736 | 737 | Slovenian. 738 | 739 | 740 | 741 | 742 | Northern Sami. 743 | 744 | 745 | 746 | 747 | Samoan. 748 | 749 | 750 | 751 | 752 | Shona. 753 | 754 | 755 | 756 | 757 | Sindhi. 758 | 759 | 760 | 761 | 762 | Somali. 763 | 764 | 765 | 766 | 767 | Southern Sotho. 768 | 769 | 770 | 771 | 772 | Spanish or Castilian. 773 | 774 | 775 | 776 | 777 | Sardinian. 778 | 779 | 780 | 781 | 782 | Swati. 783 | 784 | 785 | 786 | 787 | Sundanese. 788 | 789 | 790 | 791 | 792 | Swahili. 793 | 794 | 795 | 796 | 797 | Swedish. 798 | 799 | 800 | 801 | 802 | Tahitian. 803 | 804 | 805 | 806 | 807 | Tamil. 808 | 809 | 810 | 811 | 812 | Tatar. 813 | 814 | 815 | 816 | 817 | Telugu. 818 | 819 | 820 | 821 | 822 | Tajik. 823 | 824 | 825 | 826 | 827 | Tagalog. 828 | 829 | 830 | 831 | 832 | Thai. 833 | 834 | 835 | 836 | 837 | Tigrinya. 838 | 839 | 840 | 841 | 842 | Tonga (Tonga Islands). 843 | 844 | 845 | 846 | 847 | Tswana. 848 | 849 | 850 | 851 | 852 | Tsonga. 853 | 854 | 855 | 856 | 857 | Turkmen. 858 | 859 | 860 | 861 | 862 | Turkish. 863 | 864 | 865 | 866 | 867 | Twi. 868 | 869 | 870 | 871 | 872 | Uighur. 873 | 874 | 875 | 876 | 877 | Ukrainian. 878 | 879 | 880 | 881 | 882 | Urdu. 883 | 884 | 885 | 886 | 887 | Uzbek. 888 | 889 | 890 | 891 | 892 | Venda. 893 | 894 | 895 | 896 | 897 | Vietnamese. 898 | 899 | 900 | 901 | 902 | Volapük. 903 | 904 | 905 | 906 | 907 | Walloon. 908 | 909 | 910 | 911 | 912 | Wolof. 913 | 914 | 915 | 916 | 917 | Xhosa. 918 | 919 | 920 | 921 | 922 | Yiddish. 923 | 924 | 925 | 926 | 927 | Yoruba. 928 | 929 | 930 | 931 | 932 | Zhuang or Chuang. 933 | 934 | 935 | 936 | 937 | Zulu. 938 | 939 | 940 | 941 | 942 | -------------------------------------------------------------------------------- /ddex/tests/test_batch_generator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from os import path 3 | from tempfile import gettempdir 4 | from shutil import rmtree 5 | import DDEXUI.ddex.tests.data as data 6 | from DDEXUI.batch_generator import BatchGenerator 7 | 8 | class BatchGeneratorTests(unittest.TestCase): 9 | def test_should_generate_a_batch_containing_each_product(self): 10 | static_batch_id = "batchID" 11 | root_folder = gettempdir() 12 | expected_batch_path = path.join(root_folder, static_batch_id) 13 | rmtree(expected_batch_path, ignore_errors=True) 14 | subject = BatchGenerator(root_folder, static_batch_id) 15 | builders = [ 16 | data.valid_ddex_builder(), 17 | data.valid_ddex_builder() 18 | ] 19 | 20 | subject.generate(builders) 21 | 22 | for builder in builders: 23 | upc = builder.get_upc() 24 | expected_path = path.join(expected_batch_path, upc, upc + ".xml") 25 | self.assertTrue(path.isfile(expected_path), expected_path + " does not exist") 26 | 27 | -------------------------------------------------------------------------------- /ddex/tests/test_ddex.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from DDEXUI.ddex.ddex_builder import DDEXBuilder 3 | from DDEXUI.ddex.release_builder import ReleaseBuilder 4 | import DDEXUI.ddex.tests.data as data 5 | 6 | class DDEXBuilderTests(unittest.TestCase): 7 | def test_should_get_the_release_id(self): 8 | upc = "0748435453453" 9 | product_release = data.valid_product_release(upc) 10 | ddex_builder = DDEXBuilder().add_release(product_release) 11 | 12 | self.assertEqual(ddex_builder.get_upc(), upc) 13 | 14 | def test_should_raise_exception_if_no_product_release_exists(self): 15 | self.assertRaises(DDEXBuilder().get_upc) 16 | -------------------------------------------------------------------------------- /ddex/tests/test_ddex_builder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from DDEXUI.ddex.ddex_builder import DDEXBuilder 3 | from DDEXUI.ddex.tests.data import valid_product_release, valid_track_release 4 | 5 | class DDEXBuilderTests(unittest.TestCase): 6 | 7 | def test_the_product_release_should_be_added_to_the_start(self): 8 | upc = "0344444435356" 9 | isrc = "GB3454532345" 10 | ddex = DDEXBuilder().add_release(valid_track_release(isrc)).add_product_release(valid_product_release(upc)).build() 11 | self.assertEqual(ddex.releases[0].release_id.id, upc) 12 | -------------------------------------------------------------------------------- /ddex/tests/test_deal.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import xml.etree.cElementTree as ET 3 | from DDEXUI.ddex.deal import Deal 4 | from datetime import date 5 | 6 | class DealTests(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.use_type = "PermanentDownload" 10 | self.territory = "BE" 11 | self.start_date = date(1987,2,20) 12 | self.preorder_date = date(1987,2,19) 13 | self.preorder_preview_date = date(1987,2,18) 14 | deal = Deal("PayAsYouGoModel", self.use_type, self.territory, self.start_date, self.preorder_date, self.preorder_preview_date) 15 | self.element = deal.write() 16 | 17 | def test_should_have_commercial_model_type(self): 18 | self.assertEqual(self.element.find("./DealTerms/CommercialModelType").text, "PayAsYouGoModel") 19 | 20 | def test_should_have_use_type(self): 21 | self.assertEqual(self.element.find("./DealTerms/Usage/UseType").text, self.use_type) 22 | 23 | def test_should_have_territory_code(self): 24 | self.assertEqual(self.element.find("./DealTerms/TerritoryCode").text, self.territory) 25 | 26 | def test_should_have_start_date(self): 27 | self.assertEqual(self.element.find("./DealTerms/ValidityPeriod/StartDate").text, "1987-02-20") 28 | 29 | def test_should_have_preorder_date(self): 30 | self.assertEqual(self.element.find("./DealTerms/PreorderReleaseDate").text, "1987-02-19") 31 | 32 | def test_should_have_preorder_preview_date(self): 33 | self.assertEqual(self.element.find("./DealTerms/PreorderPreviewDate").text, "1987-02-18") 34 | -------------------------------------------------------------------------------- /ddex/tests/test_file_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from nose.tools import * 3 | from DDEXUI.file_parser import FileParser 4 | 5 | def test_generator(): 6 | cases = ([ ("ddex/tests/resources/test.mp3", "dff9465befeb68d97cd6fd103547c464", "test.mp3", "MP3"), 7 | ("ddex/tests/resources/test.jpg", "55e031153f2c0d8c63e6bf7c9baa58ba", "test.jpg", "JPG")]) 8 | for path, hash, name, extension in cases: 9 | yield check_file, path, hash, name, extension 10 | 11 | def check_file(path, hash, name, extension): 12 | file_metadata = FileParser().parse(path) 13 | assert_equal(file_metadata.md5, hash) 14 | assert_equal(file_metadata.name, name) 15 | assert_equal(file_metadata.extension, extension) 16 | 17 | class FileParserTests(unittest.TestCase): 18 | 19 | def setUp(self): 20 | self.subject = FileParser() 21 | self.file_metadata = self.subject.parse("ddex/tests/resources/test.mp3") 22 | 23 | def test_should_have_duration(self): 24 | self.assertEqual(self.file_metadata.duration, "PT0M4.000S") 25 | 26 | def test_should_have_bitrate(self): 27 | self.assertEqual(self.file_metadata.bit_rate, 64) 28 | 29 | def test_should_have_codec(self): 30 | self.assertEqual(self.file_metadata.codec, "MP3") 31 | 32 | class ImageFileParserTests(unittest.TestCase): 33 | def setUp(self): 34 | self.subject = FileParser() 35 | self.file_metadata = self.subject.parse("ddex/tests/resources/test.jpg") 36 | 37 | def test_should_have_height(self): 38 | self.assertEqual(self.file_metadata.height, 500) 39 | 40 | def test_should_have_width(self): 41 | self.assertEqual(self.file_metadata.width, 463) 42 | 43 | def test_should_have_codec(self): 44 | self.assertEqual(self.file_metadata.codec, "JPEG") 45 | -------------------------------------------------------------------------------- /ddex/tests/test_id_generators.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime 3 | from DDEXUI.ddex.ddex import generate_batch_id 4 | 5 | class TestIdGenerators(unittest.TestCase): 6 | def test_batch_id_should_be_in_expected_format(self): 7 | now = datetime.datetime(2013,12,31,23,59,30,123000) 8 | batch_id = generate_batch_id(lambda: now) 9 | self.assertEqual("20131231235930123", batch_id) 10 | 11 | -------------------------------------------------------------------------------- /ddex/tests/test_message_header.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from DDEXUI.ddex.party import * 3 | from DDEXUI.ddex.message_header import MessageHeader 4 | 5 | class MessageHeaderTests(unittest.TestCase): 6 | def setUp(self): 7 | self.subject = MessageHeader(Party('12343243', 'Sony'), Party('7777777', '7digital', PartyType.MessageRecipient)) 8 | 9 | def test_should_serialize_as_exected(self): 10 | element = self.subject.write() 11 | self.assertEqual(element.tag, 'MessageHeader') 12 | self.assertNotEqual(element.find("./MessageThreadId").text, None) 13 | self.assertNotEqual(element.find("./MessageId").text, None) 14 | self.assertNotEqual(element.find("./MessageCreatedDateTime"),None) 15 | self.assertNotEqual(element.find("./MessageSender"),None) 16 | self.assertNotEqual(element.find("./MessageRecipient"),None) 17 | -------------------------------------------------------------------------------- /ddex/tests/test_party.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from DDEXUI.ddex.party import * 3 | 4 | class PartyTests(unittest.TestCase): 5 | def setUp(self): 6 | self.party = Party('gdfg42jkdz', 'Sony') 7 | 8 | def test_should_serialise_correctly(self): 9 | print(self.party) 10 | party_element = self.party.write() 11 | self.assertEqual(party_element.find('./PartyId').text, 'gdfg42jkdz') 12 | self.assertEqual(party_element.find('./PartyName/FullName').text, 'Sony') 13 | 14 | 15 | -------------------------------------------------------------------------------- /ddex/tests/test_party_repository.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import configparser 3 | from DDEXUI.ddex.party import * 4 | from DDEXUI.party_repository import * 5 | import sqlite3 6 | 7 | 8 | class PartyRepositoryTests(unittest.TestCase): 9 | def setUp(self): 10 | connection = self.__get_connection() 11 | connection.execute("DROP TABLE IF EXISTS party") 12 | connection.execute("CREATE TABLE IF NOT EXISTS party(name text, partyId text, PartyType integer)") 13 | connection.close() 14 | self.party = Party('IDIDIDO', 'Some Label Name', PartyType.MessageSender) 15 | 16 | def __get_connection(self): 17 | return sqlite3.connect("ddexui") 18 | 19 | def tearDown(self): 20 | c = self.__get_connection() 21 | cu = c.cursor() 22 | cu.execute("DROP TABLE IF EXISTS party") 23 | c.close() 24 | 25 | def test_it_should_return_the_party(self): 26 | connection = self.__get_connection() 27 | connection.execute("INSERT INTO party(name, partyId, partyType) VALUES(?,?,?)", (self.party.name, self.party.party_id, self.party.party_type)) 28 | connection.commit() 29 | connection.close() 30 | 31 | party = PartyRepository().get_party(PartyType.MessageSender) 32 | self.assertEqual(party, self.party) 33 | 34 | def test_it_should_return_none_if_there_is_no_party(self): 35 | self.assertEqual(PartyRepository().get_party(PartyType.MessageSender), None) 36 | 37 | def test_it_should_write_the_party(self): 38 | repo = PartyRepository() 39 | repo.write_party(self.party) 40 | 41 | self.assertEqual(repo.get_party(PartyType.MessageSender), self.party) 42 | 43 | def test_should_not_overwrite_other_parties_when_saving(self): 44 | connection = self.__get_connection() 45 | connection.execute("INSERT INTO party(name, partyId, partyType) VALUES(?,?,?)", (self.party.name, self.party.party_id, self.party.party_type)) 46 | connection.commit() 47 | connection.close() 48 | repo = PartyRepository() 49 | party = Party("GSDFGDFGSEG", "SomeParty", PartyType.MessageRecipient) 50 | repo.write_party(party) 51 | self.assertNotEqual(repo.get_party(PartyType.MessageSender), None) 52 | self.assertNotEqual(repo.get_party(PartyType.MessageRecipient), None) 53 | -------------------------------------------------------------------------------- /ddex/tests/test_release.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | #todo figure out how to mock things 3 | from DDEXUI.ddex.release import * 4 | import xml.etree.cElementTree as ET 5 | 6 | class Test(unittest.TestCase): 7 | def setUp(self): 8 | self.name = "Bob" 9 | self.upc = "0132384103241" 10 | self.cline = "Copyright brillient music" 11 | self.pline = "Published by brillient music" 12 | self.year = 2013 13 | self.release_reference = "R0" 14 | self.release_type = "Single" 15 | self.artist_name = "Marty McFly and the hoverboards" 16 | self.genres = ["Rock", "Pop"] 17 | self.label = "Tru Thoughts" 18 | self.explicit = True 19 | self.release = (Release( 20 | self.name, 21 | self.cline, 22 | self.pline, 23 | self.year, 24 | self.release_reference, 25 | ReleaseId(1, self.upc), 26 | self.release_type, 27 | self.artist_name, 28 | self.label, 29 | self.explicit) 30 | ) 31 | self.release.genres = self.genres 32 | 33 | self.element = self.release.write() 34 | 35 | def test_all_genres_should_be_written(self): 36 | genre_elements = self.element.findall("./ReleaseDetailsByTerritory/Genre/GenreText") 37 | genres = list(map(lambda el: el.text, genre_elements)) 38 | self.assertEqual(["Rock","Pop"], genres) 39 | 40 | def test_title_text_should_be_written(self): 41 | self.assertEqual(self.name, self.element.find("./ReferenceTitle/TitleText").text) 42 | self.assertEqual(self.name, self.element.find("./ReleaseDetailsByTerritory/Title[@TitleType='FormalTitle']/TitleText").text) 43 | self.assertEqual(self.name, self.element.find("./ReleaseDetailsByTerritory/Title[@TitleType='GroupingTitle']/TitleText").text) 44 | self.assertEqual(self.name, self.element.find("./ReleaseDetailsByTerritory/Title[@TitleType='DisplayTitle']/TitleText").text) 45 | 46 | def test_upc_should_be_written(self): 47 | self.assertEqual(self.upc, self.element.find("./ReleaseId/ICPN").text) 48 | 49 | def test_release_reference_should_be_set(self): 50 | self.assertEqual(self.release_reference, self.element.find("./ReleaseReference").text) 51 | 52 | def test_release_refernce_territory_code_should_be_worldwide(self): 53 | self.assertEqual("Worldwide",self.element.find("./ReleaseDetailsByTerritory/TerritoryCode").text) 54 | 55 | def test_pline_should_be_written(self): 56 | self.assertEqual(self.pline,self.element.find("./PLine/PLineText").text) 57 | 58 | def test_cline_should_be_written(self): 59 | self.assertEqual(self.cline,self.element.find("./CLine/CLineText").text) 60 | 61 | def test_year_should_be_written(self): 62 | self.assertEqual(str(2013), self.element.find("./CLine/Year").text) 63 | self.assertEqual(str(2013), self.element.find("./PLine/Year").text) 64 | 65 | def test_release_type_should_be_written(self): 66 | self.assertEqual(self.release_type, self.element.find("./ReleaseType").text) 67 | 68 | def test_label_should_be_written(self): 69 | self.assertEqual(self.label, self.element.find("./ReleaseDetailsByTerritory/LabelName").text) 70 | 71 | def test_artist_name_should_be_written(self): 72 | self.assertEqual(self.artist_name, self.element.find("./ReleaseDetailsByTerritory/DisplayArtistName").text) 73 | self.assertEqual(self.artist_name, self.element.find("./ReleaseDetailsByTerritory/DisplayArtist/PartyName/FullName").text) 74 | 75 | def test_artist_role_should_be_written(self): 76 | self.assertEqual("MainArtist", self.element.find("./ReleaseDetailsByTerritory/DisplayArtist/ArtistRole").text) 77 | 78 | def test_parental_warning_should_be_written_as_explicit(self): 79 | path = "./ReleaseDetailsByTerritory/ParentalWarningType" 80 | self.assertEqual("Explicit", self.element.find(path).text) 81 | element = polite_release = Release("","","",1,"",ReleaseId(1,"000000000000"),"","","",False).write() 82 | self.assertEqual("NotExplicit", element.find(path).text) 83 | 84 | def test_should_write_deals(self): 85 | self.release.add_deal(MockDeal()) 86 | self.release.add_deal(MockDeal()) 87 | release_deal = self.release.write_deals() 88 | self.assertEqual(release_deal.find("./DealReleaseReference").text, self.release_reference) 89 | self.assertEqual(len(release_deal.findall("./Deal")), 2) 90 | 91 | def test_should_write_resource_references(self): 92 | ref = "A0" 93 | self.release.add_resource_reference(ref) 94 | element = self.release.write() 95 | resource_refs = element.findall("./ReleaseResourceReferenceList/ReleaseResourceReference") 96 | 97 | self.assertEqual(len(resource_refs), 1) 98 | self.assertEqual(resource_refs[0].text, ref) 99 | resource_group_content_items = element.findall("./ReleaseDetailsByTerritory/ResourceGroup/ResourceGroupContentItem") 100 | self.assertEqual(len(resource_group_content_items), 1) 101 | content_item = resource_group_content_items[0] 102 | self.assertEqual(content_item.find("./SequenceNumber").text, "1") 103 | self.assertEqual(content_item.find("./ResourceType").text, "SoundRecording") 104 | self.assertEqual(content_item.find("./ReleaseResourceReference").text, ref) 105 | 106 | class MockDeal: 107 | def write(self): 108 | return ET.Element("Deal") 109 | -------------------------------------------------------------------------------- /ddex/tests/test_release_builder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from DDEXUI.ddex.release import ReleaseIdType, Release 3 | from DDEXUI.ddex.release_builder import ReleaseBuilder 4 | from DDEXUI.ddex.tests.data_helper import TestData 5 | 6 | class ReleaseBuilderTests(unittest.TestCase): 7 | def test_can_build_valid_release(self): 8 | release = TestData.release_builder().build() 9 | 10 | self.assertIsInstance(release, Release) 11 | 12 | def test_errors_if_a_non_string_resource_reference_is_passed_in(self): 13 | self.assertRaises(TypeError, lambda: ReleaseBuilder().reference(123)) 14 | 15 | def test_can_get_isrc(self): 16 | isrc = "FR132131234" 17 | release_builder = ReleaseBuilder().release_id(ReleaseIdType.Isrc, isrc) 18 | 19 | self.assertEqual(release_builder.get_isrc(), isrc) 20 | 21 | def test_can_get_title(self): 22 | title = "Thriller" 23 | release_builder = ReleaseBuilder().title(title) 24 | 25 | self.assertEqual(release_builder.get_title(), title) 26 | 27 | def test_releases_should_only_add_resource_references_once(self): 28 | subject = ReleaseBuilder() 29 | reference = "R0" 30 | subject.add_resource(reference) 31 | subject.add_resource(reference) 32 | 33 | release = subject.build() 34 | 35 | resource_references = list(map(lambda x: x[0], release.release_resource_references)) 36 | self.assertTrue(resource_references.count(reference) == 1, resource_references) 37 | -------------------------------------------------------------------------------- /ddex/tests/test_release_id.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.release import ReleaseId 2 | import unittest 3 | 4 | class ReleaseIdTests(unittest.TestCase): 5 | def test_it_should_serialise_upc_ids(self): 6 | upc = "123432222222" 7 | id = ReleaseId(1, upc).write() 8 | self.assertEqual(id.text, upc) 9 | #todo check the element name and attrs 10 | 11 | def test_it_should_serialise_upc_ids(self): 12 | isrc = "123432222222" 13 | id = ReleaseId(2, isrc).write() 14 | self.assertEqual(id.text, isrc) 15 | #todo check the element name and attrs 16 | -------------------------------------------------------------------------------- /ddex/tests/test_resource.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import functools 3 | import xml.etree.cElementTree as ET 4 | from DDEXUI.ddex.file_metadata import AudioFileMetadata, ImageFileMetadata 5 | from DDEXUI.ddex.resource import SoundRecording, Image 6 | import os 7 | 8 | class SoundRecordingTests(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.resource_reference = "A1" 12 | self.title = "Some Title" 13 | self.file_metadata = AudioFileMetadata("PT0H2M28.000S", 320,"dff9465befeb68d97cd6fd103547c464","test.mp3", "MP3") 14 | self.technical_resource_details_reference = "T1" 15 | self.res = SoundRecording(self.resource_reference, "abc", self.title, self.file_metadata, self.technical_resource_details_reference) 16 | self.element = self.res.write() 17 | 18 | def test_resource_should_display_type(self): 19 | self.assertEqual(self.element.tag, "SoundRecording") 20 | 21 | def test_resource_should_display_sound_recording_type(self): 22 | self.assertEqual(self.element.find("./SoundRecordingType").text, "MusicalWorkSoundRecording") 23 | 24 | def test_resource_should_contain_isrc(self): 25 | self.assertEqual(self.element.find("./SoundRecordingId/ISRC").text, "abc") 26 | 27 | def test_resource_should_contain_resource_reference(self): 28 | self.assertEqual(self.element.find("./ResourceReference").text, self.resource_reference) 29 | 30 | def test_resource_should_contain_reference_title(self): 31 | self.assertEqual(self.element.find("./ReferenceTitle/TitleText").text, self.title) 32 | 33 | def test_should_have_a_worldwide_territory(self): 34 | self.assertEqual(self.element.find("./SoundRecordingDetailsByTerritory/TerritoryCode").text, "Worldwide") 35 | 36 | def test_should_have_audio_codec(self): 37 | self.assertEqual(self.world_wide_territory().find("./TechnicalSoundRecordingDetails/AudioCodecType").text, "MP3") 38 | 39 | def test_should_have_file_name_and_path(self): 40 | file_element = self.world_wide_territory().find("./TechnicalSoundRecordingDetails/File") 41 | self.assertEqual(file_element.find("./FileName").text, "test.mp3") 42 | hash_sum = file_element.find("./HashSum") 43 | self.assertEqual(hash_sum.find("./HashSum").text, "dff9465befeb68d97cd6fd103547c464") 44 | self.assertEqual(hash_sum.find("./HashSumAlgorithmType").text, "MD5") 45 | 46 | def test_should_have_duration(self): 47 | self.assertEqual(self.element.find("./Duration").text, "PT0H2M28.000S") 48 | 49 | def test_should_have_technical_resource_details_reference(self): 50 | self.assertEqual(self.world_wide_territory().find("./TechnicalSoundRecordingDetails/TechnicalResourceDetailsReference").text, self.technical_resource_details_reference) 51 | 52 | def test_should_store_technical_resource_details_reference(self): 53 | self.assertEqual(self.res.technical_resource_details_reference, self.technical_resource_details_reference) 54 | 55 | def world_wide_territory(self): 56 | return (list(filter(lambda x: x.find("./TerritoryCode").text == "Worldwide", self.element 57 | .findall("./SoundRecordingDetailsByTerritory")))[0]) 58 | 59 | class ImageTests(unittest.TestCase): 60 | 61 | def setUp(self): 62 | self.resource_reference = "A1" 63 | self.title = "Some Title" 64 | self.file_metadata = ImageFileMetadata("dff9465befeb68d97cd6fd103547c464","test.jpg", "JPG", 300, 400) 65 | self.technical_resource_details_reference = "T1" 66 | self.res = Image(self.resource_reference, "abc", self.file_metadata, self.technical_resource_details_reference) 67 | self.element = self.res.write() 68 | 69 | def test_resource_should_display_type(self): 70 | self.assertEqual(self.element.tag, "Image") 71 | 72 | def test_resource_should_display_image_type(self): 73 | self.assertEqual(self.element.find("./ImageType").text, "FrontCoverImage") 74 | 75 | def test_resource_should_contain_id(self): 76 | el = self.element.find("./ImageId/ProprietaryId") 77 | self.assertEqual(el.text, "abc") 78 | self.assertEqual(el.attrib["Namespace"], "DDEXUI") 79 | 80 | def test_resource_should_contain_resource_reference(self): 81 | self.assertEqual(self.element.find("./ResourceReference").text, self.resource_reference) 82 | 83 | def test_should_have_a_worldwide_territory(self): 84 | self.assertEqual(self.element.find("./ImageDetailsByTerritory/TerritoryCode").text, "Worldwide") 85 | 86 | def test_should_have_image_codec(self): 87 | self.assertEqual(self.world_wide_territory().find("./TechnicalImageDetails/ImageCodecType").text, "JPEG") 88 | 89 | def test_should_have_image_height_and_width(self): 90 | self.assertEqual(self.world_wide_territory().find("./TechnicalImageDetails/ImageWidth").text, str(self.file_metadata.width)) 91 | self.assertEqual(self.world_wide_territory().find("./TechnicalImageDetails/ImageHeight").text, str(self.file_metadata.height)) 92 | 93 | def test_should_have_file_name_and_path(self): 94 | file_element = self.world_wide_territory().find("./TechnicalImageDetails/File") 95 | self.assertEqual(file_element.find("./FileName").text, "test.jpg") 96 | hash_sum = file_element.find("./HashSum") 97 | self.assertEqual(hash_sum.find("./HashSum").text, "dff9465befeb68d97cd6fd103547c464") 98 | self.assertEqual(hash_sum.find("./HashSumAlgorithmType").text, "MD5") 99 | 100 | def test_should_have_technical_resource_details_reference(self): 101 | self.assertEqual(self.world_wide_territory().find("./TechnicalImageDetails/TechnicalResourceDetailsReference").text, self.technical_resource_details_reference) 102 | 103 | def test_should_store_technical_resource_details_reference(self): 104 | self.assertEqual(self.res.technical_resource_details_reference, self.technical_resource_details_reference) 105 | 106 | def world_wide_territory(self): 107 | return (list(filter(lambda x: x.find("./TerritoryCode").text == "Worldwide", self.element 108 | .findall("./ImageDetailsByTerritory")))[0]) 109 | -------------------------------------------------------------------------------- /ddex/tests/test_resource_manager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from shutil import rmtree 3 | from os import path 4 | from tempfile import gettempdir 5 | import uuid 6 | from DDEXUI.file_parser import FileParser 7 | from DDEXUI.ddex.resource import SoundRecording, Image 8 | from DDEXUI.resource_manager import ResourceManager 9 | 10 | class ResourceManagerSoundRecordingTests(unittest.TestCase): 11 | @classmethod 12 | def setUpClass(self): 13 | self.upc = "49024343245" 14 | self.isrc = "FR343245" 15 | rmtree(self.upc, ignore_errors=True) 16 | self.root_folder = gettempdir() 17 | self.batch_id = str(uuid.uuid4()) 18 | self.title = "the title" 19 | file_path = path.join('ddex', 'tests', 'resources', 'test.mp3') 20 | self.resource_reference = "A1" 21 | self.technical_resource_details_reference = "T1" 22 | 23 | self.expected = SoundRecording(self.resource_reference, self.isrc, self.title, FileParser().parse(file_path), self.technical_resource_details_reference) 24 | 25 | self.subject = ResourceManager(FileParser(), self.batch_id, self.root_folder) 26 | 27 | self.resource = self.subject.add_sound_recording(self.upc, file_path, self.isrc, self.title, self.resource_reference, self.technical_resource_details_reference) 28 | 29 | def test_should_copy_file_to_product_resources_folder(self): 30 | expected_path = path.join(self.root_folder, self.batch_id, self.upc, 'resources', "{}_{}.mp3".format(self.isrc, self.technical_resource_details_reference)) 31 | self.assertTrue(path.isfile(expected_path), "expected {} to exist".format(expected_path)) 32 | 33 | def test_should_create_resource_with_isrc(self): 34 | self.assertEqual(self.resource.isrc, self.expected.isrc) 35 | 36 | def test_should_create_resource_with_title(self): 37 | self.assertEqual(self.resource.title, self.expected.title) 38 | 39 | def test_should_create_resource_with_resource_reference(self): 40 | self.assertEqual(self.resource.resource_reference(), self.resource_reference) 41 | 42 | def test_should_create_resource_with_technical_resource_details_reference(self): 43 | self.assertEqual(self.resource.technical_resource_details_reference, self.technical_resource_details_reference) 44 | 45 | def test_should_create_resource_with_file(self): 46 | self.assertEqual(self.resource.file_metadata.md5, self.expected.file_metadata.md5) 47 | 48 | 49 | class ResourceManagerImageTests(unittest.TestCase): 50 | @classmethod 51 | def setUpClass(self): 52 | self.upc = "49024343245" 53 | self.isrc = "FR343245" 54 | rmtree(self.upc, ignore_errors=True) 55 | self.root_folder = gettempdir() 56 | self.batch_id = str(uuid.uuid4()) 57 | self.title = "the title" 58 | file_path = path.join('ddex', 'tests', 'resources', 'test.jpg') 59 | self.resource_reference = "A2" 60 | self.technical_resource_details_reference = "T4" 61 | 62 | self.expected = Image(self.resource_reference, self.upc, FileParser().parse(file_path), '') 63 | 64 | self.subject = ResourceManager(FileParser(), self.batch_id, self.root_folder) 65 | 66 | self.resource = self.subject.add_image(self.upc, file_path, self.resource_reference, self.technical_resource_details_reference) 67 | 68 | def test_should_copy_file_to_product_resources_folder(self): 69 | expected_path = path.join(self.root_folder, self.batch_id, self.upc, 'resources', self.upc+'.jpg') 70 | self.assertTrue(path.isfile(expected_path)) 71 | 72 | def test_should_create_resource_with_upc(self): 73 | self.assertEqual(self.resource.id_value(), self.upc) 74 | 75 | def test_should_create_resource_with_file(self): 76 | self.assertEqual(self.resource.file_metadata.md5, self.expected.file_metadata.md5) 77 | 78 | def test_should_create_resource_with_resource_reference(self): 79 | self.assertEqual(self.resource.resource_reference(), self.resource_reference) 80 | 81 | def test_should_create_resource_with_technical_resource_details_reference(self): 82 | self.assertEqual(self.resource.technical_resource_details_reference, self.technical_resource_details_reference) 83 | -------------------------------------------------------------------------------- /ddex/tests/test_validate.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.validate import Validate 2 | from datetime import datetime 3 | import unittest 4 | 5 | class ValidateTests(unittest.TestCase): 6 | def test_upcs_must_only_contain_numbers(self): 7 | result = Validate().upc("abc123456789") 8 | self.assertEqual(result["error"], "upc must only contain numbers") 9 | self.assertEqual(result["success"], False) 10 | 11 | def test_upcs_cannot_contain_less_than_12_digits(self): 12 | result = Validate().upc("12345") 13 | self.assertEqual(result["error"], "upc must be 12 - 13 digits long") 14 | self.assertEqual(result["success"], False) 15 | 16 | def test_upcs_cannot_contain_more_than_13_digits(self): 17 | result = Validate().upc("12345678910235") 18 | self.assertEqual(result["error"], "upc must be 12 - 13 digits long") 19 | self.assertEqual(result["success"], False) 20 | 21 | def test_valid_upcs_do_not_return_errors(self): 22 | result = Validate().upc("123456789012") 23 | self.assertEqual(result["value"], "123456789012") 24 | self.assertEqual(result["success"], True) 25 | self.assertFalse("error" in result) 26 | 27 | def test_year_must_be_a_number(self): 28 | result = Validate().year("a") 29 | self.assertEqual(result["error"], "year must be a number") 30 | self.assertEqual(result["success"], False) 31 | 32 | def test_a_valid_year_does_not_return_any_errors(self): 33 | result = Validate().year("2012") 34 | self.assertEqual(result["value"], 2012) 35 | self.assertFalse("error" in result) 36 | 37 | def test_strings_cannot_be_empty(self): 38 | result = Validate().not_empty("") 39 | self.assertEqual(result["error"], "value cannot be empty") 40 | self.assertEqual(result["success"], False) 41 | 42 | def test_non_empty_strings_are_fine(self): 43 | result = Validate().not_empty("hello") 44 | self.assertEqual(result["value"], "hello") 45 | self.assertEqual(result["success"], True) 46 | 47 | def test_date_must_be_in_corret_format(self): 48 | result = Validate().date("abc") 49 | self.assertEqual(result["error"], "date must be in format YYYY-mm-dd") 50 | 51 | def test_dates_do_not_return_errors(self): 52 | result = Validate().date("2012-01-22") 53 | self.assertEqual(result["value"], datetime(2012,1,22)) 54 | self.assertIsInstance(result["value"], datetime) 55 | self.assertFalse("error" in result) 56 | 57 | -------------------------------------------------------------------------------- /ddex/tests/test_validates_against_ddex_schema.py: -------------------------------------------------------------------------------- 1 | import lxml.etree as ET 2 | from DDEXUI.ddex.ddex import DDEX 3 | from DDEXUI.ddex.release import Release, ReleaseId 4 | from DDEXUI.ddex.party import Party, PartyType 5 | from DDEXUI.ddex.deal import Deal 6 | from DDEXUI.ddex.resource import SoundRecording, Image 7 | from DDEXUI.ddex.message_header import MessageHeader 8 | from DDEXUI.file_parser import FileParser 9 | from datetime import date 10 | import unittest 11 | 12 | class DDEXSchemaValidation(unittest.TestCase): 13 | def test_created_ddex_files_validate_against_ddex_xsd(self): 14 | #helped by http://alex-sansom.info/content/validating-xml-against-xml-schema-python 15 | output_file = "/tmp/file.xml" 16 | 17 | release = self.create_product_release() 18 | 19 | sound_recording = self.create_sound_recording() 20 | 21 | image_resource = self.create_image() 22 | resources = [sound_recording, image_resource] 23 | release.add_resource_reference(sound_recording.resource_reference()) 24 | release.add_resource_reference(image_resource.resource_reference(), "SecondaryResource") 25 | releases = [release] 26 | 27 | DDEX(Party('derwwfefw', 'Sony'), Party("34545345", "7digital", PartyType.MessageRecipient),releases, resources).write(output_file) 28 | 29 | tree = ET.parse(output_file) 30 | #original schema at http://ddex.net/xml/ern/341/release-notification.xsd 31 | schema = ET.XMLSchema(file="ddex/tests/resources/xsds/release-notification.xsd") 32 | schema.assertValid(tree) 33 | 34 | def create_product_release(self): 35 | release = (Release( 36 | "Bad", 37 | "copyright MJ", 38 | "Published by MJ", 39 | 1987, 40 | "R0", 41 | ReleaseId(1,"1234567898764"), 42 | "Album", 43 | "Michael Jackson", 44 | "Epic", 45 | True)) 46 | 47 | deal = Deal("PayAsYouGoModel", "PermanentDownload", "FR", date(2012,1,3)) 48 | 49 | release.add_deal(deal) 50 | return release 51 | 52 | def create_sound_recording(self): 53 | resource_reference = "A1" 54 | resource = SoundRecording(resource_reference, "abc", "Bad", FileParser().parse("ddex/tests/resources/test.mp3"),"T1") 55 | return resource 56 | 57 | def create_image(self): 58 | image_resource_reference = "A2" 59 | image_resource = Image(image_resource_reference, "abc", FileParser().parse("ddex/tests/resources/test.jpg"),"T2") 60 | return image_resource 61 | -------------------------------------------------------------------------------- /ddex/validate.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as datetime 2 | 3 | class Validate: 4 | 5 | def upc(self, text): 6 | result = {} 7 | result["success"] = False 8 | if(len(text) < 12 or len(text) > 13): 9 | result["error"] = "upc must be 12 - 13 digits long" 10 | for char in text: 11 | if(not self.__number(char)): 12 | result["error"] = "upc must only contain numbers" 13 | if(not "error" in result): 14 | result["value"] = text 15 | result["success"] = True 16 | return result 17 | 18 | def year(self, text): 19 | result = {} 20 | result["success"] = False 21 | if(not self.__number(text)): 22 | result["error"] = "year must be a number" 23 | if(not "error" in result): 24 | result["value"] = int(text) 25 | result["success"] = True 26 | return result 27 | 28 | 29 | def __number(self, text): 30 | try: 31 | int(text) 32 | return True 33 | except ValueError: 34 | return False 35 | 36 | def not_empty(self, text): 37 | result = {} 38 | if(text == ""): 39 | result["error"] = "value cannot be empty" 40 | result["success"] = False 41 | else: 42 | result["success"] = True 43 | result["value"] = text 44 | return result 45 | 46 | 47 | def date(self, text): 48 | result = {} 49 | date_format = "%Y-%m-%d" 50 | try: 51 | result["value"] = datetime.strptime(text, date_format) 52 | result["success"] = True 53 | except: 54 | result["error"] = "date must be in format YYYY-mm-dd" 55 | result["success"] = False 56 | return result 57 | -------------------------------------------------------------------------------- /deal_window.py: -------------------------------------------------------------------------------- 1 | import DDEXUI.ddex.deal as deal 2 | from DDEXUI.inputs import * 3 | from DDEXUI.ddex.validate import Validate 4 | from DDEXUI.tkinterutil import showerrorbox 5 | 6 | class DealWindow(tk.tkinter.Toplevel): 7 | def __init__(self, frame): 8 | tk.tkinter.Toplevel.__init__(self, frame) 9 | self.title("Deal Editor") 10 | self.focus_set() 11 | self.fields = ([OptionInput(self, "Commercial Model", *deal.CommercialModals), 12 | OptionInput(self, "Use Type", *deal.UseTypes), 13 | OptionInput(self, "Territory", *deal.Territories), 14 | EntryInput(self, "Start Date", Validate().date), 15 | EntryInput(self, "Pre Order Date", Validate().date), 16 | EntryInput(self, "Pre Order Preview Date", Validate().date)]) 17 | for i in range(len(self.fields)): 18 | self.fields[i].draw(i) 19 | tk.Button(self, text="OK", command=self.__destroy_if_valid).grid(row=len(self.fields)+1, column=0) 20 | 21 | def __destroy_if_valid(self): 22 | if(self.all_release_fields_valid()): 23 | self.destroy() 24 | 25 | @showerrorbox 26 | def create_deal(self): 27 | return (deal.Deal(self.value_of("Commercial Model"), 28 | self.value_of("Use Type"), 29 | self.value_of("Territory"), 30 | self.value_of("Start Date"), 31 | self.value_of("Pre Order Date"), 32 | self.value_of("Pre Order Preview Date"))) 33 | 34 | #todo: remove duplication of these 2 methods 35 | def value_of(self, title): 36 | row = next(filter(lambda x: x.title == title,self.fields)) 37 | return row.value() 38 | 39 | def all_release_fields_valid(self): 40 | all_valid = True 41 | for row in self.fields: 42 | all_valid = all_valid and row.on_validate() 43 | return all_valid 44 | -------------------------------------------------------------------------------- /file_parser.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.file_metadata import * 2 | from mutagenx.mp3 import MP3 3 | from PIL import Image 4 | import hashlib 5 | import os 6 | 7 | class FileParser(): 8 | 9 | def parse(self, file_path): 10 | hash = self.__get_hash(file_path) 11 | path = os.path.split(file_path)[1] 12 | extension = self.get_extension(file_path) 13 | 14 | if(extension == "MP3"): 15 | mp3 = MP3(file_path) 16 | return (AudioFileMetadata(self.__get_duration(mp3.info.length), 17 | self.__get_bitrate(mp3.info.bitrate), 18 | hash, 19 | path, 20 | extension)) 21 | if(extension == "JPG"): 22 | img = Image.open(file_path) 23 | return ImageFileMetadata(hash, path, extension, img.size[0], img.size[1]) 24 | 25 | def __get_duration(self, total_seconds): 26 | minutes = int(total_seconds / 60) 27 | seconds = int(total_seconds % 60) 28 | return "PT" + str(minutes) + "M" + str(seconds) + ".000S" 29 | 30 | def __get_bitrate(self, bit_rate): 31 | return int(bit_rate / 1000) 32 | 33 | def __get_hash(self, file_path): 34 | hash = hashlib.md5() 35 | with open(file_path, 'rb') as resource: 36 | hash.update(resource.read()) 37 | return hash.hexdigest() 38 | 39 | @staticmethod 40 | def get_extension(file_path): 41 | return os.path.splitext(file_path)[1].replace(".","").upper() 42 | -------------------------------------------------------------------------------- /inputs.py: -------------------------------------------------------------------------------- 1 | import tkinter.ttk as tk 2 | 3 | class InputRow: 4 | def __init__(self, frame, title): 5 | self.frame = frame 6 | self.error_label = tk.tkinter.Label(self.frame, fg="red", width=50) 7 | self.v = tk.tkinter.StringVar() 8 | self.title = title 9 | self.input = None 10 | 11 | def value(self): 12 | return self.v.get() 13 | 14 | def on_validate(self): 15 | return True 16 | 17 | def draw(self, row): 18 | self.input.grid(row=row, column=1) 19 | self.error_label.grid(row=row, column=2) 20 | 21 | class OptionInput(InputRow): 22 | def __init__(self, frame, title, *args): 23 | InputRow.__init__(self, frame, title) 24 | self.v.set(args[0]) 25 | self.input = tk.OptionMenu(self.frame, self.v, args[0], *args) 26 | self.label = tk.Label(self.frame,text=title) 27 | 28 | def draw(self,row): 29 | InputRow.draw(self, row) 30 | self.label.grid(row=row, column=0) 31 | 32 | class CheckboxInput(InputRow): 33 | def __init__(self, frame, title): 34 | InputRow.__init__(self, frame, title) 35 | self.v = tk.tkinter.BooleanVar() 36 | self.v.set(False) 37 | self.input = tk.Checkbutton(self.frame, variable=self.v, text=title) 38 | 39 | class EntryInput(InputRow): 40 | def __init__(self, frame, title, validation_function): 41 | InputRow.__init__(self, frame, title) 42 | self.validation_function = validation_function 43 | self.label = tk.Label(self.frame,text=title) 44 | self.input = (tk.Entry( 45 | self.frame, 46 | width=20, 47 | textvariable=self.v, 48 | validate="focusout", 49 | validatecommand=self.on_validate, 50 | invalidcommand=lambda: self.on_invalidate(self.validation_function(self.v.get())["error"]))) 51 | self.v.set("2121211111141") 52 | 53 | def is_valid(self): 54 | return self.validation_function(self.v.get())["success"] == True 55 | 56 | def value(self): 57 | return self.validation_function(self.v.get())["value"] 58 | 59 | 60 | def draw(self,row): 61 | InputRow.draw(self, row) 62 | self.label.grid(row=row, column=0,sticky=tk.tkinter.W) 63 | 64 | def on_invalidate(self, message): 65 | self.error_label["text"] = message 66 | 67 | def on_validate(self): 68 | valid = self.is_valid() 69 | if(valid): 70 | self.error_label["text"] = "" 71 | return valid 72 | -------------------------------------------------------------------------------- /metadata_form.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.3 2 | import tkinter.ttk as tk 3 | import tkinter.messagebox as mb 4 | from DDEXUI.ddex.ddex_builder import DDEXBuilder 5 | from DDEXUI.ddex.party import * 6 | from DDEXUI.ddex.validate import Validate 7 | from DDEXUI.party_repository import PartyRepository 8 | from DDEXUI.inputs import * 9 | from DDEXUI.release_window import ProductReleaseWindow 10 | from DDEXUI.batch_generator import BatchGenerator 11 | from DDEXUI.ddex.ddex import generate_batch_id 12 | from DDEXUI.tkinterutil import showerrorbox 13 | import sys 14 | import os 15 | 16 | class PartyWindow(tk.tkinter.Toplevel): 17 | def __init__(self, frame, party_type): 18 | #http://tkinter.unpythonic.net/wiki/ModalWindow 19 | self.party_repository = PartyRepository() 20 | self.party_type = party_type 21 | tk.tkinter.Toplevel.__init__(self, frame) 22 | # self.geometry("400x300") 23 | self.transient(frame) 24 | self.focus_set() 25 | #self.grab_set() 26 | message = "Please enter your " + PartyType.reverse_mapping[self.party_type] + " ddex party details. You can apply for a ddex Party id for free at: http://ddex.net/content/implementation-licence-application-form" 27 | text = tk.tkinter.Label(self, height=5, text=message, wraplength=400) 28 | text.grid(row=0, column=0,columnspan=3) 29 | self.party_id = EntryInput(self, "Party Id", Validate().not_empty) 30 | self.party_name = EntryInput(self, "Party Name", Validate().not_empty) 31 | self.party_id.draw(2) 32 | self.party_name.draw(3) 33 | tk.Button(self, text="OK", command=self.save_and_close).grid(row=4, column=0) 34 | frame.wait_window(self) 35 | 36 | 37 | def save_and_close(self): 38 | if(self.party_id.on_validate() and self.party_name.on_validate()): 39 | party = Party(self.party_id.value(), self.party_name.value(), self.party_type) 40 | self.party_repository.write_party(party) 41 | self.destroy() 42 | 43 | class Program: 44 | def __init__(self): 45 | self.party_repository = PartyRepository() 46 | self._ddex_builders = [] 47 | self.frame = tk.tkinter.Tk() 48 | self.frame.geometry("600x300") 49 | icon = tk.tkinter.PhotoImage(file=self.get_icon()) 50 | self.frame.tk.call("wm", "iconphoto", self.frame._w, icon) 51 | self.frame.title("Metadata Editor") 52 | self.product_list = tk.tkinter.Listbox(self.frame) 53 | self.product_list.bind('', lambda x: self.remove_product()) 54 | self._root_folder = "out" 55 | self.add_release_button = tk.Button(self.frame, text="Add Product", command=self.create_ddex) 56 | self.button = tk.Button(self.frame, text="OK", command=self.write_ddex) 57 | self.remove_button = tk.Button(self.frame, text="Remove", command=self.remove_product, state="disabled") 58 | self._batch_id = generate_batch_id() 59 | self._batch_generator = BatchGenerator(self._root_folder, self._batch_id) 60 | 61 | @showerrorbox 62 | def write_ddex(self): 63 | self.__check_for_party(PartyType.MessageSender) 64 | self.__check_for_party(PartyType.MessageRecipient) 65 | sender = self.party_repository.get_party(PartyType.MessageSender) 66 | recipient = self.party_repository.get_party(PartyType.MessageRecipient) 67 | for builder in self._ddex_builders: 68 | ddex = builder.sender(sender).recipient(recipient) 69 | self._batch_generator.generate(self._ddex_builders) 70 | mb.showinfo("DDEXUI", "your ddex files have been created") 71 | 72 | def get_icon(self): 73 | if getattr(sys, 'frozen', False): 74 | resources = os.path.join(os.path.dirname(sys.executable), 'res') 75 | else: 76 | resources = os.path.join(os.path.dirname(__file__), 'res') 77 | return os.path.join(resources, 'favicon.gif') 78 | 79 | @showerrorbox 80 | def create_ddex(self): 81 | release_window = ProductReleaseWindow(self.frame, self._root_folder, self._batch_id) 82 | release_window.wait_window() 83 | ddex_builder = release_window.create_ddex() 84 | self._ddex_builders.append(ddex_builder) 85 | self.product_list.insert(tk.tkinter.END, ddex_builder.get_upc()) 86 | self.remove_button['state'] = 'enabled' 87 | 88 | @showerrorbox 89 | def remove_product(self): 90 | selected = self.product_list.curselection() 91 | if(self.product_list.size() == 0 or len(selected) == 0): 92 | return 93 | self.product_list.delete(selected[0]) 94 | self._ddex_builders.pop(int(selected[0])) 95 | if(self.product_list.size() == 0): 96 | self.remove_button['state'] = 'disabled' 97 | 98 | def __check_for_party(self, party_type): 99 | if(self.party_repository.get_party(party_type) is None): 100 | PartyWindow(self.frame, party_type) 101 | 102 | def main(self): 103 | self.product_list.grid(row=0, column=0) 104 | self.add_release_button.grid(row=1, column=0) 105 | self.remove_button.grid(row=2, column=0) 106 | self.button.grid(row=3, column=0) 107 | self.__check_for_party(PartyType.MessageSender) 108 | self.frame.mainloop() 109 | 110 | Program().main() 111 | -------------------------------------------------------------------------------- /party_repository.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.party import Party 2 | import sqlite3 3 | 4 | class PartyRepository: 5 | def __init__(self): 6 | self.__with_cursor(lambda cursor, connection: cursor.execute("CREATE TABLE IF NOT EXISTS party(name text, partyId text, partyType integer)")) 7 | 8 | def __get_connection(self): 9 | return sqlite3.connect("ddexui") 10 | 11 | def get_party(self, party_type): 12 | connection = self.__get_connection() 13 | cursor = connection.cursor() 14 | cursor.execute("SELECT partyId, name, partyType FROM party WHERE partyType=?", (party_type,)) 15 | party = cursor.fetchone() 16 | connection.close() 17 | if(party == None): 18 | return None 19 | return Party(party[0], party[1], party[2]) 20 | 21 | def write_party(self, party): 22 | self.__with_cursor(lambda cursor, connection: self.__write_party(cursor, connection, party)) 23 | 24 | def __write_party(self, cursor, connection, party): 25 | cursor.execute("INSERT INTO party(name, partyId, partyType) VALUES(?,?,?)", (party.name, party.party_id, party.party_type,)) 26 | connection.commit() 27 | connection.close() 28 | 29 | def __with_cursor(self, action): 30 | connection = self.__get_connection() 31 | cur = connection.cursor() 32 | action(cur, connection) 33 | connection.close() 34 | -------------------------------------------------------------------------------- /product_service.py: -------------------------------------------------------------------------------- 1 | from DDEXUI.ddex.ddex_builder import DDEXBuilder 2 | 3 | class ProductService: 4 | def __init__(self, product_release_builder, upc, coverart_path, track_builder_file_paths, is_update, resource_manager): 5 | self._product_release_builder = product_release_builder 6 | self.upc = upc 7 | self.coverart_path = coverart_path 8 | self.track_builder_file_paths = track_builder_file_paths 9 | self.is_update = is_update 10 | self._resource_manager = resource_manager 11 | self.ddex_builder = DDEXBuilder() 12 | self.ddex_builder.update(self.is_update) 13 | 14 | def _add_image(self, upc): 15 | if(self.coverart_path != None): 16 | image = self._resource_manager.add_image(upc, self.coverart_path, "A0", "T0") 17 | self._product_release_builder.add_resource(image.resource_reference()) 18 | self.ddex_builder.add_resource(image) 19 | 20 | def _add_audio_resources(self, upc, file_paths, builder): 21 | for path in file_paths: 22 | resource = self._resource_manager.add_sound_recording(upc, path, builder.get_isrc(), builder.get_title(), "A"+str(self.resource_count), "T"+str(self.resource_count)) 23 | self.resource_count += 1 24 | builder.add_resource(resource.resource_reference()) 25 | self._product_release_builder.add_resource(resource.resource_reference()) 26 | self.ddex_builder.add_resource(resource) 27 | 28 | def create_ddex(self): 29 | self._add_image(self.upc) 30 | count = 1 31 | self.resource_count = 1 32 | for track in self.track_builder_file_paths: 33 | self._add_audio_resources(self.upc, track.paths, track.builder) 34 | self.ddex_builder.add_release(track.builder.reference("R" + str(count)).build()) 35 | count += 1 36 | self.ddex_builder.add_product_release(self._product_release_builder.build()) 37 | return self.ddex_builder 38 | -------------------------------------------------------------------------------- /release_window.py: -------------------------------------------------------------------------------- 1 | import tkinter.ttk as tk 2 | from PIL import Image, ImageTk 3 | from tkinter.filedialog import askopenfilename 4 | from DDEXUI.ddex.release_builder import ReleaseBuilder 5 | from DDEXUI.ddex.validate import Validate 6 | from DDEXUI.inputs import * 7 | from DDEXUI.ddex.release import * 8 | from DDEXUI.deal_window import DealWindow 9 | from DDEXUI.file_parser import FileParser 10 | from DDEXUI.tkinterutil import showerrorbox 11 | from DDEXUI.resource_manager import ResourceManager 12 | from DDEXUI.product_service import ProductService 13 | 14 | class ReleaseWindow(tk.tkinter.Toplevel): 15 | def __init__(self, frame): 16 | tk.tkinter.Toplevel.__init__(self, frame) 17 | self._release_builder = ReleaseBuilder() 18 | self.fields = ([ 19 | EntryInput(self, "Title", Validate().not_empty), 20 | EntryInput(self, "Year", Validate().year), 21 | EntryInput(self, "C Line", Validate().not_empty), 22 | EntryInput(self, "P Line", Validate().not_empty), 23 | EntryInput(self, "Artist", Validate().not_empty), 24 | EntryInput(self, "Label", Validate().not_empty), 25 | CheckboxInput(self, "Explicit") 26 | ]) 27 | 28 | def draw_fields(self): 29 | for i in range(len(self.fields)): 30 | self.fields[i].draw(i) 31 | 32 | def create_deal(self): 33 | deal_window = DealWindow(self) 34 | deal_window.wait_window() 35 | deal = deal_window.create_deal() 36 | self._release_builder.add_deal(deal) 37 | 38 | class ProductReleaseWindow(ReleaseWindow): 39 | def __init__(self, frame, root_folder, batch_id): 40 | ReleaseWindow.__init__(self, frame) 41 | self.track_builder_file_paths = [] 42 | self.image_path = None 43 | self._resource_manager = ResourceManager(FileParser(), batch_id, root_folder) 44 | self.fields.append(EntryInput(self, "UPC", Validate().upc)) 45 | self.fields.append(OptionInput(self, "Type", 'Single', 'Album')) 46 | self.is_update_check_box = CheckboxInput(self, "Is Update") 47 | self.fields.append(self.is_update_check_box) 48 | self.total_fields = len(self.fields) 49 | self.draw_fields() 50 | self.add_deal_button = tk.Button(self, text="Add deal", command=self.create_deal) 51 | self.add_deal_button.grid(row=self.new_row(), column=0) 52 | self.add_track_button = tk.Button(self, text="Add Track", command=self.create_track) 53 | self.add_track_button.grid(row=self.new_row(), column=0) 54 | self.delete_track_button = tk.Button(self, text="Remove Track", state="disabled", command=self.remove_track) 55 | self.delete_track_button.grid(row=self.new_row(), column=0) 56 | self.add_img_button = tk.Button(self, text="Album Artwork", command=self.add_image).grid(row=self.new_row(), column=0) 57 | self.button = tk.Button(self, text="OK", command=self.__destroy_if_valid).grid(row=self.new_row(), column=0) 58 | self.track_list = tk.tkinter.Listbox(self) 59 | self.track_list.bind('', lambda x: self.remove_track()) 60 | track_list_row = self.new_row() 61 | self.track_list.grid(row=track_list_row, column=0) 62 | 63 | self.artwork = tk.tkinter.Label(self) 64 | self.artwork.grid(row=track_list_row, column=1) 65 | self.draw_tracks() 66 | 67 | def new_row(self): 68 | self.total_fields += 1 69 | return self.total_fields 70 | 71 | def add_image(self): 72 | self.image_path = askopenfilename(filetypes=[("JPG files", "*.jpg")]) 73 | im = Image.open(self.image_path) 74 | im.thumbnail((200, 200)) 75 | i = ImageTk.PhotoImage(im) 76 | self.artwork.configure(image=i) 77 | self.artwork.image = i 78 | 79 | def draw_tracks(self): 80 | for track in self.track_builder_file_paths: 81 | self.track_list.insert(tk.tkinter.END, track.builder.get_title()) 82 | 83 | def value_of(self, title): 84 | row = next(filter(lambda x: x.title == title, self.fields)) 85 | return row.value() 86 | 87 | def __destroy_if_valid(self): 88 | if(self.all_release_fields_valid()): 89 | self.destroy() 90 | 91 | def _populate_product_release(self, upc): 92 | (self._release_builder.title(self.value_of("Title")) 93 | .c_line(self.value_of("C Line")) 94 | .p_line(self.value_of("P Line")) 95 | .year(self.value_of("Year")) 96 | .reference("R0") 97 | .release_id(ReleaseIdType.Upc, upc) 98 | .release_type(self.value_of("Type")) 99 | .artist(self.value_of("Artist")) 100 | .label(self.value_of("Label")) 101 | .parental_warning(self.value_of("Explicit"))) 102 | 103 | def create_ddex(self): 104 | upc = self.value_of("UPC") 105 | self._populate_product_release(upc) 106 | product_service = (ProductService(self._release_builder, 107 | upc, self.image_path, 108 | self.track_builder_file_paths, 109 | self.is_update_check_box.value(), 110 | self._resource_manager)) 111 | return product_service.create_ddex() 112 | 113 | @showerrorbox 114 | def create_track(self): 115 | track_window = TrackReleaseWindow(self) 116 | track_window.wait_window() 117 | track_builder_file_path = track_window.create_release() 118 | self.track_builder_file_paths.append(track_builder_file_path) 119 | self.track_list.insert(tk.tkinter.END, track_builder_file_path.builder.get_title()) 120 | self.delete_track_button['state'] = 'enabled' 121 | 122 | @showerrorbox 123 | def remove_track(self): 124 | selected = self.track_list.curselection() 125 | if(self.track_list.size() == 0 or len(selected) == 0): 126 | return 127 | print(selected) 128 | self.track_list.delete(selected[0]) 129 | self.track_builder_file_paths.pop(int(selected[0])) 130 | if(self.track_list.size() == 0): 131 | self.delete_track_button['state'] = 'disabled' 132 | 133 | def all_release_fields_valid(self): 134 | all_valid = True 135 | if(self.is_update_check_box.value() != True): 136 | all_valid = self.image_path is not None and self.image_path is not "" 137 | 138 | for row in self.fields: 139 | all_valid = all_valid and row.on_validate() 140 | return all_valid 141 | 142 | class TrackReleaseWindow(ReleaseWindow): 143 | def __init__(self, frame): 144 | ReleaseWindow.__init__(self, frame) 145 | self._sound_file_paths = [] 146 | self.fields.append(EntryInput(self, "ISRC", Validate().not_empty)) 147 | total_fields = len(self.fields) 148 | self.draw_fields() 149 | self.add_sound_recording_button = tk.Button(self, text="Add Audio", command=self.add_audio).grid(row=total_fields+1, column=0) 150 | self.add_deal_button = tk.Button(self, text="Add deal", command=self.create_deal).grid(row=total_fields+2, column=0) 151 | self.button = tk.Button(self, text="OK", command=self.__destroy_if_valid).grid(row=total_fields+3, column=0) 152 | 153 | def add_audio(self): 154 | file_path = askopenfilename(filetypes=(("Audio files", "*.mp3"), ("Audio files", "*.flac"))) 155 | if(file_path is not ""): 156 | self._sound_file_paths.append(file_path) 157 | 158 | def value_of(self, title): 159 | row = next(filter(lambda x: x.title == title, self.fields)) 160 | return row.value() 161 | 162 | def __destroy_if_valid(self): 163 | if(self.all_release_fields_valid()): 164 | self.destroy() 165 | 166 | def create_release(self): 167 | builder = (self._release_builder.title(self.value_of("Title")) 168 | .c_line(self.value_of("C Line")) 169 | .p_line(self.value_of("P Line")) 170 | .year(self.value_of("Year")) 171 | .reference("R0") 172 | .release_id(ReleaseIdType.Isrc,self.value_of("ISRC")) 173 | .release_type("TrackRelease") 174 | .artist(self.value_of("Artist")) 175 | .label(self.value_of("Label")) 176 | .parental_warning(self.value_of("Explicit"))) 177 | return TrackBuilderFilePath(self._sound_file_paths, builder) 178 | 179 | def all_release_fields_valid(self): 180 | all_valid = True 181 | for row in self.fields: 182 | all_valid = all_valid and row.on_validate() 183 | return all_valid 184 | 185 | class TrackBuilderFilePath: 186 | def __init__(self, paths, builder): 187 | self.paths = paths 188 | self.builder = builder 189 | 190 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mutagenx>=1.21 2 | nose==1.3.0 3 | pillow==2.2.1 4 | lxml==3.2.0 5 | -------------------------------------------------------------------------------- /res/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willm/DDEXUI/c10e879cf1a32bcd26005f215f0693d3015b9f29/res/favicon.gif -------------------------------------------------------------------------------- /resource_manager.py: -------------------------------------------------------------------------------- 1 | from shutil import copyfile 2 | from DDEXUI.file_parser import FileParser 3 | from os import path, makedirs 4 | from DDEXUI.ddex.resource import SoundRecording, Image 5 | 6 | class ResourceManager: 7 | def __init__(self, file_parser, batch_id, root_folder='.'): 8 | self._batch_id = batch_id 9 | self._root_folder = root_folder 10 | self._file_parser = file_parser 11 | 12 | def add_sound_recording(self, upc, file_path, isrc, title, resource_reference, technical_resource_details_reference): 13 | file_name = self.__file_name_from("{}_{}".format(isrc, technical_resource_details_reference), file_path) 14 | moved_file_path = self.__copy_file(upc, file_path, file_name) 15 | return SoundRecording(resource_reference, isrc, title, self._file_parser.parse(moved_file_path), technical_resource_details_reference) 16 | 17 | def add_image(self, upc, file_path, resource_reference, technical_resource_details_reference): 18 | file_name = self.__file_name_from(upc, file_path) 19 | moved_file_path = self.__copy_file(upc, file_path, file_name) 20 | return Image(resource_reference, upc, self._file_parser.parse(moved_file_path), technical_resource_details_reference) 21 | 22 | def __copy_file(self, upc, src_file_path, dst_file_name): 23 | resources_directory = path.join(self._root_folder, self._batch_id, upc, 'resources') 24 | moved_file_path = path.join(resources_directory, dst_file_name) 25 | if(not path.isdir(resources_directory)): 26 | makedirs(resources_directory) 27 | copyfile(src_file_path, moved_file_path) 28 | return moved_file_path 29 | 30 | def __file_name_from(self, name, file_path): 31 | return name + '.' + FileParser.get_extension(file_path).lower() 32 | -------------------------------------------------------------------------------- /run_tests.cmd: -------------------------------------------------------------------------------- 1 | nosetests -v 2 | pause 3 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #python3 -m unittest discover 2 | nosetests -v 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from cx_Freeze import setup, Executable 2 | from os import path 3 | 4 | executables = [ 5 | Executable('metadata_form.py') 6 | ] 7 | 8 | includes = [path.join('res', 'favicon.gif')] 9 | print(includes) 10 | 11 | setup(name='DDEXUI', 12 | version='0.1', 13 | description='A user interface for distributing ddex deliveries', 14 | executables=executables, 15 | options = {'build_exe': {'include_files': includes}} 16 | ) 17 | -------------------------------------------------------------------------------- /tkinterutil.py: -------------------------------------------------------------------------------- 1 | import tkinter.ttk as tk 2 | import tkinter.messagebox as mb 3 | 4 | #thanks to http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/ 5 | #and http://stackoverflow.com/questions/6666882/tkinter-python-catching-exceptions 6 | def showerrorbox(func): 7 | def run(*args, **kwargs): 8 | try: 9 | func(*args, **kwargs) 10 | except Exception as e: 11 | print(e) 12 | mb.showerror("Error", e) 13 | raise e 14 | return run 15 | -------------------------------------------------------------------------------- /unpackEgg.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pkg_resources 3 | import sys 4 | from setuptools.archive_util import unpack_archive 5 | 6 | def unpackEgg(modulo): 7 | eggs = pkg_resources.require(modulo) 8 | for egg in eggs: 9 | if os.path.isdir(egg.location): 10 | sys.path.insert(0, egg.location) 11 | continue 12 | unpack_archive(egg.location, ".") 13 | eggpacks = set() 14 | eggspth = open("./eggs.pth", "w") 15 | for egg in eggs: 16 | eggspth.write(os.path.basename(egg.location)) 17 | eggspth.write("\n") 18 | eggpacks.update(egg.get_metadata_lines("top_level.txt")) 19 | eggspth.close() 20 | 21 | eggpacks.clear() 22 | 23 | if __name__ == '__main__': 24 | unpackEgg(sys.argv[1]) 25 | 26 | --------------------------------------------------------------------------------