├── .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 | [](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 |
--------------------------------------------------------------------------------